写在前面
本文难度偏中下,涉及到的点大多为如何在项目中合理应用TS,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用
什么是 TS
对函数参数的类型限制;
对数组和对象的类型限制,避免定义出错 例如数据解构复杂或较多时,可能会出现数组定义错误 a = { }, if (a.length){ // xxxxx }
let functionA = 'jiawen' // 实际上 let functionA: string = 'jiawen'
怎么用 TS
在业务中如何用TS/如何用好TS?这个问题其实和 " 在业务中怎么用好一个API " 是一样的。首先要知道这个东西在干嘛,参数是什么,规则是什么,能够接受有哪些扩展......等等。简而言之,撸它!哪些扩展......等等。 简而言之,撸它!
let a: string = 'jiawen';let flag: boolean = false;let num: number = 150interface IState: { flag: boolean; name: string; num: number;}
// 元组类型表示已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。
let x: [string, number];
x = ['jiawen', 18]; // ok
x = [18, 'jiawen']; // Erro
console.log(x[0]); // jiawen
let special: string = undefined
// 值得一提的是 undefined/null 是所有基本类型的子类,
// 所以它们可以任意赋值给其他已定义的类型,这也是为什么上述代码不报错的原因
// object 表示的是常规的 Javascript对象类型,非基础数据类型
const offDuty = (value: object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // Error
// {} 表示的是 非null / 非undefined 的任意类型
const offDuty = (value: {}) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // ok
// {} 和Object几乎一致,区别是Object会对Object内置的 toString/hasOwnPreperty 进行校验
const offDuty = (value: Object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // Error
如果需要一个对象类型,但对属性没有要求,建议使用 object
{} 和 Object 表示的范围太大,建议尽量不要使用
// 我们通常在业务中可多采用点状对象函数(规定参数对象类型)
const offDuty = (value: { x: number; y: string }) => {
console.log("x is ", value.x);
console.log("y is ", value.y);
}
// 业务中一定会涉及到"可选属性";先简单介绍下方便快捷的“可选属性”
const offDuty = (value: { x: number; y?: string }) => {
console.log("必选属性x ", value.x);
console.log("可选属性y ", value.y);
console.log("可选属性y的方法 ", value.y.toLocaleLowerCase());
}
offDuty({ x: 123, y: 'jiawen' })
offDuty({ x: 123 })
// 提问:上述代码有问题吗?
答案:
// offDuty({ x: 123 }) 会导致结果报错value.y.toLocaleLowerCase()
// Cannot read property 'toLocaleLowerCase' of undefined
方案1: 手动类型检查
const offDuty = (value: { x: number; y?: string }) => {
if (value.y !== undefined) {
console.log("可能不存在的 ", value.y.toUpperCase());
}
}
方案2:使用可选属性 (推荐)
const offDuty = (value: { x: number; y?: string }) => {
console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
}
// unknown 可以表示任意类型,但它同时也告诉TS, 开发者对类型也是无法确定,做任何操作时需要慎重
let Jiaven: unknown
Jiaven.toFixed(1) // Error
if (typeof Jiaven=== 'number') {
Jiaven.toFixed(1) // OK
}
当我们使用any类型的时候,any会逃离类型检查,并且any类型的变量可以执行任意操作,编译时不会报错
anyscript === javascript
注意:any 会增加了运行时出错的风险,不到万不得已不要使用;
如果遇到想要表示【不知道什么类型】的场景,推荐优先考虑 unknown
union也叫联合类型,由两个或多个其他类型组成,表示可能为任何一个的值,类型之间用 ' | '隔开
type dayOff = string | number | boolean
联合类型的隐式推导可能会导致错误,遇到相关问题请参考语雀 code and tips —— 《TS的隐式推导》
.值得注意的是,如果访问不共有的属性的时候,会报错,访问共有属性时不会.上个最直观的demo
function dayOff (value: string | number): number {
return value.length;
}
// number并不具备length,会报错,解决方法:typeof value === 'string'
function dayOff (value: string | number): number {
return value.toString();
}
// number和string都具备toString(),不会报错
// never是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
// 那never在实际开发中到底有什么作用?这里笔者原汁原味照搬尤雨溪的经典解释来做第一个例子
第一个例子,当你有一个 union type:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
在 switch 当中判断 type,TS是可以收窄类型的 (discriminated union):
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。
如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:
type All = Foo | Bar | Baz
然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,
这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。
所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。
第二个用法 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
第三个用法 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}
interface IProps {
onOK: () => void
}
void 和 undefined 功能高度类似,但void表示对函数的返回值并不在意或该方法并无返回值
笔者认为ts中的enum是一个很有趣的枚举类型,它的底层就是number的实现
1.普通枚举
enum Color {
Red,
Green,
Blue
};
let c: Color = Color.Blue;
console.log(c); // 2
2.字符串枚举
enum Color {
Red = 'red',
Green = 'not red',
};
3.异构枚举 / 有时也叫混合枚举
enum Color {
Red = 'red',
Num = 2,
};
<第一个坑>
enum Color {
A, // 0
B, // 1
C = 20, // 20
D, // 21
E = 100, // 100
F, // 101
}
若初始化有部分赋值,那么后续成员的值为上一个成员的值加1
<第二个坑> 这个坑是第一个坑的延展,稍不仔细就会上当!
const getValue = () => {
return 23
}
enum List {
A = getValue(),
B = 24, // 此处必须要初始化值,不然编译不通过
C
}
console.log(List.A) // 23
console.log(List.B) // 24
console.log(List.C) // 25
如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。
否则将会 Enum member must have initializer.
笔者理解的泛型很白话:先不指定具体类型,通过传入的参数类型来得到具体类型 我们从下述的 filter-demo 入手,探索一下为什么一定需要泛型
泛型的基础样式
function fun<T>(args: T): T {
return args
}
如果没接触过,是不是会觉得有点懵?没关系!我们直接从业务角度深入。
1.刚开始的需求:过滤数字类型的数组
declare function filter(
array: number[],
fn: (item: unknown) => boolean
) : number[];
2.产品改了需求:还要过滤一些字符串 string[]
彳亍,那就利用函数的重载, 加一个声明, 虽然笨了点,但是很好理解
declare function filter(
array: string[],
fn: (item: unknown) => boolean
): string[];
declare function filter(
array: number[],
fn: (item: unknown) => boolean
): number[];
3.产品又来了! 这次还要过滤 boolean[]、object[] ..........
这个时候如果还是选择重载,将会大大提升工作量,代码也会变得越来越累赘,这个时候泛型就出场了,
它从实现上来说更像是一种方法,通过你的传参来定义类型,改造如下:
declare function filter<T>(
array: T[],
fn: (item: unknown) => boolean
): T[];
泛型中的<T>可以是任意,但是大部分偏好为T、U、S等,
type Foo<T, U = string> = { // 多参数、默认值
foo: Array<T> // 可以传递
bar: U
}
type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }
既然是“函数”,那也会有“限制”,下文列举一些稍微常见的约束。
1. extends: 限制 T 必须至少是一个 XXX 的类型
type dayOff<T extends HTMLElement = HTMLElement> = {
where: T,
name: string
}
2. Readonly<T>: 构造一个所有属性为readonly,这意味着无法重新分配所构造类型的属性。
interface Eat {
food: string;
}
const todo: Readonly<Eat> = {
food: "meat beef milk",
};
todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.
3. Pick<T,K>: 从T中挑选出一些K属性
interface Todo {
name: string;
job: string;
work: boolean;
type TodoPreview = Pick<Todo, "name" | "work">;
const todo: TodoPreview = {
name: "jiawen",
work: true,
};
todo;
4. Omit<T, K>: 结合了 T 和 K 并忽略对象类型中 K 来构造类型。
interface Todo {
name: string;
job: string;
work: boolean;
}
type TodoPreview = Omit<Todo, "work">;
const todo: TodoPreview = {
name: "jiawen",
job: 'job',
};
5.Record: 约束 定义键类型为 Keys、值类型为 Values 的对象类型。
enum Num {
A = 10001,
B = 10002,
C = 10003
}
const NumMap: Record<Num, string> = {
[Num.A]: 'this is A',
[Num.B]: 'this is B'
}
// 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003",
// 但类型 "Record<ErrorCodes, string>" 中需要该属性,所以我们还可以通过Record来做全面性检查
keyof 关键字可以用来获取一个对象类型的所有 key 类型
type User = {
id: string;
name: string;
};
type UserKeys = keyof User; // "id" | "name"
改造如下
type Record<K extends keyof any, T> = {
[P in K]: T;
};
此时的 T 为 any;
还有一些不常用,但是很易懂的:
6. Extract<T, U> 从T,U中提取相同的类型
7. Partial<T> 所有属性可选
type User = {
id?: string,
gender: 'male' | 'female'
}
type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'}
type Partial<T> = { [U in keyof T]?: T[U] }
8. Required<T> 所有属性必须 << === >> 与Partial相反
type User = {
id?: string,
sex: 'male' | 'female'
}
type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
function showUserProfile (user: RequiredUser) {
console.log(user.id) // 这时候就不需要再加?了
console.log(user.sex)
}
type Required<T> = { [U in keyof T]-?: T[U] }; -? : 代表去掉?
TS的一些须知
interface User { id: string}interface User { name: string}const user = {} as Userconsole.log(user.id);console.log(user.name);
type User = {
id: string,
}
if (true) {
type User = {
name: string,
}
const user = {} as User;
console.log(user.name);
console.log(user.id) // 类型“User”上不存在属性“id”。
}
GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error
declare var GlobalStore: {
foo: string;
};
GlobalStore.foo = "foo";
GlobalStore.bar = "bar";
declare global {
var GlobalStore: {
foo: string;
bar: string;
};
}
export {}; // export 关键字改变文件的模式
interface Props {
[key: string]: number
}
<Component count={1} /> // OK
<Component count={true} /> // Error
<Component count={'1'} /> // Error
type User = {
userId: string
friendList: {
fristName: string
lastName: string
}[]
}
type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }
type group = [number, string]
type First = group[0] // number
type Second = group[1] // string
function getLength(value: string | number): number {
if (value.length) {
return value.length;
} else {
return value.toString().length;
}
// 这个问题在object of parmas已经提及,不再赘述
修改后:
if ((<string>value).length) {
return (<string>value).length;
} else {
return something.toString().length;
}
}
断言的两种写法
1. <类型>值: <string>value
2. 或者 value as string
特别注意!!!断言成一个联合类型中不存在的类型是不允许的
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
function liveDangerously(x?: number | undefined | null) {
// 推荐写法
console.log(x!.toFixed());
}
如何在 Hook 组件中使用 TS
// 这里ts可以推断 value的类型并且能对setValue函数调用进行约束
const [value, setValue] = useState(0);
interface MyObject {
name: string;
age?: number;
}
// 这里需要传递MyObject才能约束 value, setValue
// 所以我们一般情况下推荐传入类型
const [value, setValue] = useState<MyObject>(null);
const value = 10;
const result = useMemo(() => value * 2, [value]); // 推断出result是number类型
const multiplier = 2;
// 推断出 (value: number) => number
// 注意函数入参value需要定义类型
const multiply = useCallback((value: number) => value * multiplier, [multiplier]);
1. 如果传值为null
const MyInput = () => {
const inputRef = useRef<HTMLInputElement>(null); // 这里约束inputRef是一个html元素
return <input ref={inputRef} />
}
2. 如果不为null
const myNumberRef = useRef(0); // 自动推断出 myNumberRef.current 是number类型
myNumberRef.current += 1;
type Theme = 'light' | 'dark';// 我们在createContext就传了类型了const ThemeContext = createContext<Theme>('dark');const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider>)const MyComponent = () => { // useContext根据ThemeContext推断出类型,这里不需要显示传 const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>
一些思考
想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=bbs
同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:https://github.com/DTStack