导读
工作现在的做的大部分前端项目都是使用ts编写的,但平时工作时只是把ts当成类型标记语言来编写,对于花里胡哨的ts构造与定义经常抓瞎。本文从最基础的ts基础开始,复习巩固一些ts最基本的概念和语法,同时介绍一些常见的ts体操方法,来走入TS体操的大门。
TS基础
父类型与子类型
在ts类型系统中,子类型并不需要显式,对父子类型满足如下关系
-
子类型比父类型更加具体,父类型比子类型更抽象
-
子类型可以赋值给父类型
对于第一点,是否具体和抽象,需要区分情况来看。比较简单的一个例子就是对于接口和联合类型的处理不同,看以下例子
// 父类型
interface Animal {
weight: number;
}
// 子类型
interface Dog {
weight: number;
bark: () => void;
}
// 父类型
type Animals = 'dog' | 'eagle' | 'seagull'
// 子类型
type Birds = 'eagle' | 'seagull'
首先看interface Animal和Dog这个例子。Animal只有一个weight体重属性,比较宽泛,每一个动物都会有体重。而Dog除了weight之外,还多了一个bark属性,说明我们的Dog会叫了,而不是所有的Animal都会叫。所以Dog类型更加具体了,这里的Dog就是Animal的子类型。
而对于第二个联合类型的例子,乍一看起来好像是Animals里属性比Birds多,Animals应该是子类型,但实际正相反。因为对于Animals来说,dog、eagle、seagull这几个值他都可以接受,而Birds则只能接eagle、seagull这两个值,更加具体了,所以Birds是Animals的子类型。
而对于第二点,我们看另一个例子
// 这里的Animal和Dog定义同上文保持一致
// 子类型
type AnimalHandler = (animal:Animal) => void
// 父类型
type DogHandler = (dog:Dog) => void
let animalHandler: AnimalHandler;
let dogHandler: DogHandler;
animalHandler = dogHandler // error
dogHandler = animalHandler // ok
这个例子乍一看谁更具体谁更抽象看不太出来,因此我们从可赋值性来考虑。现在如果将animalHandler赋值给dogHandler,也就是说原本应该接受一个Dog参数的函数变成了接受一个Animal参数,如果我们在函数中调用了参数的bark()方法,显然Animal参数是满足不了的,不能赋值。如果将dogHandler赋值给animalHandler,Animal能做到的Dog一定能做到,可以满足赋值。所以在这里DogHandler是AnimalHandler的父类型。
逆变与协变
逆变与协变的定义如下
协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
定义有点晦涩,但也算好理解,我们可以换个说法
具有父子关系的类型,在经过类型构造处理后,所产生的新类型之间父子关系是否颠倒。如果父子关系颠倒,就是发生了逆变;未颠倒则是发生了协变
我们在上边举的例子中Animal => AnimalHandler、Dog =>DogHandler这个操作,Handler之间和原本的Animal和Dog之间的父子关系发生了颠倒,就是一个逆变操作,即type ToFunctionParam<T> = (arg:T) => void 这个构造。
协变操作也很好理解,例如函数的返回值type ToReturnType<T> = () => T 就是一个协变操作。
对于父子类型和协变逆变也可以参考github.com/sl1673495/b…
常用关键字
any、unknown与never
never是所有类型的子类型,可赋值给所有类型。但是没有类型可以赋值给never,即使是any也不行。
同时never还有一个很有意思的特点,在联合类型中never会被当做一个空类型,可以理解为一个单独的 | ,看下方样例
let neverValue: never;
neverValue = 'never' as any; // error Type 'any' is not assignable to type 'never'
neverValue = 'never' as never;
let num:number = neverValue; // ok
type ab = 'a' | 'b'| never; // 结果为 'a'|'b'
unknown是所有类型的父类型,所有类型可以给unknown赋值。但是unknown类型当赋值给其他类型(any除外)时,会进行类型检查,要求将unknow收窄到具体的类型。
let unknownValue: unknown = 'str'; // ok
unknownValue = 123; //ok
let numValue: number = unknownValue //error Type 'unknown' is not assignable to type 'number'
let numValue: number = unknownValue as number // ok
let anyValue: any = unknownValue //ok
any代表任何类型,和unknown很像,但是不会进行类型收窄,比unknown更宽松。也就是说所有类型(包括never与unknown)都可以给any赋值,any可以给所有类型赋值(除了上文提到的never)
let anyValue: any = 'value' as unknown // ok
anyValue = 'value' as never // ok
let nerverValue: never = anyValue //error Type 'any' is not assignable to type 'never'
extends
extends在ts中主要有两个作用
- 接口继承
这个比较好理解,看如下例子
interface T1 { name: string }
interface T2 { sex: number }
// {name:string; sex:number; age:number}
interface T3 extends T1,T2 { age: number }
- 条件判断
ts体操中最常见的用法,用法和三元表达式很像,A extends B ? T : P ,判断A是否能赋值给B,也即是A是否是B的子类型。如果能则返回T,不能则返回P,举几个例子
type isNumber<T> = T extends number ? true : false
isNumber<number> // true
isNumber<1> // true
type test<T> = T extends {} ? true : false // 同样,如果这里的{}换成unknown也总会返回true
test<string> // true
test<number> // true
对于联合类型的处理,extends的处理比较特殊。在 A extends B 中,如果A是一个联合类型,则会将A拆分开一个一个去执行extends操作,最后将拆分执行出的结果联合起来,看几个例子
// isNumber定义同上
type t1 = isNumber<number|string> //结果是boolean,即true|false
// 内置工具Exclude,作用是从联合类型中排除几个类型来构造新类型
type Exclude<T, U> = T extends U ? never : T;
type test = Exclude<'a'|'b'|'c'|'d','a'|'b'> // never|never|'c'|'d',即'c'|'d'
如果不想让联合类型被拆分,可以用[]套起来
type t1 = isNumber<[number|string]> //false
infer
用来推断类型,只能用于extends关键词中,表示在 extends 条件语句中待推断的类型变量,例如内置的Parameters和ReturnType工具类型
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P: never
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
keyof
keyof T 会得到由 T 上已知的公共属性名组成的联合类型。
type test = keyof {a:number;b:number} // 'a'|'b'
in
用于遍历联合类型,类似与for ... In,例如内置的Partial工具类型,将所有属性变成可选
type Partial<T> = {
[P in keyof T]?: T[P]; // P will be each key of T
}
体操常见方法
类型提取
使用extends和infer从复杂类型中提取出需要,通常按照语法返回infer推测出的值即可
// 推测Promise的返回值
type UnPromisefy<T extends Promise<unknown>> = T extends Promise<infer P> ? P :never
type test = UnPromisefy<Promise<number>> // number
对于数组类型可以使用...解构符来操作
type PopArr<Arr extends unknown[]> =
Arr extends [] ? [] : Arr extends [...infer Rest, unknown] ? Rest : never;
类型扩展
基于原有的类型做变换,构造出一个新的类型
// 字符串首字母大写
type CapitalizeStr<Str extends string> =
Str extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : Str;
// 添加函数的入参类型(新构造的函数会丢掉参数名)
type AppendArgument<Fn extends Function, Type> =
(...args: [...Parameters<Fn>, Type]) => ReturnType<Fn>
这里特别提一下对于函数参数的处理,通常对于函数参数的操作是使用...解构(不解构ts是推断不出来的),或者直接使用内置的Parameters工具类,处理的效果如下
这里的[a: number, b:number]有命名的元组(labled/named tuple),等效于[number, number],使用起来也是和元组一样用index来访问,而并不能用他的key直接访问,可以参考What are “named or labeled tuples” in Typescript?,关于ts中的数组和元祖可以参考Arrays & Tuples — Type-Level TypeScript
递归循环
ts中虽然有in循环,但只能用在联合类型上,使用起来相对有局限。所以通常循环需要使用递归来实现,尤其是对于数组和字符串的处理
// 下划线转小驼峰
type ToCamelCase<Str extends string> =
Str extends `${infer Left}_${infer Right}${infer Rest}` ?
`${Left}${Uppercase<Right>}${ToCamelCase<Rest>}`
: Str
// 数组是否包含某类型
type Includes<Arr extends unknown[], FindItem> =
Arr extends [infer First, ...infer Rest]
? IsEqual<First, FindItem> extends true
? true
: Includes<Rest, FindItem>
: false;
type-challenges
了解了ts体操的基础知识,就可以开始做体操啦
可以在www.typescriptlang.org/zh/play 这个playground中做题
祝大家做操愉快