下面是 TypeScript 进阶内容
官方工具类 操作接口类型 Partial Partial 工具类型可以将一个类型的所有属性变为可选的,且该工具类型返回的类型是给定类型的所有子集,下面我们看一个具体的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Partial <T> = { [P in keyof T]?: T[P] } interface Person { name : string age ?: number weight ?: number } type PartialPerson = Partial <Person >interface PartialPerson { name ?: string age ?: number weight ?: number }
Required 与 Partial 工具类型相反,Required 工具类型可以将给定类型的所有属性变为必填的,下面我们看一个具体示例。
1 2 3 4 5 6 7 8 9 10 type Required <T> = { [P in keyof T]-?: T[P] } type RequiredPerson = Required <Person >interface RequiredPerson { name : string age : number weight : number }
Readonly Readonly 工具类型可以将给定类型的所有属性设为只读,这意味着给定类型的属性不可以被重新赋值,下面我们看一个具体的示例。
1 2 3 4 5 6 7 8 9 10 type Readonly <T> = { readonly [P in keyof T]: T[P] } type ReadonlyPerson = Readonly <Person >interface ReadonlyPerson { readonly name : string readonly age ?: number readonly weight ?: number }
Pick Pick 工具类型可以从给定的类型中选取出指定的键值,然后组成一个新的类型,下面我们看一个具体的示例。
1 2 3 4 5 6 7 8 9 type Pick <T, K extends keyof T> = { [P in K]: T[P] } type NewPerson = Pick <Person , 'name' | 'age' >interface NewPerson { name : string age ?: number }
Omit 与 Pick 类型相反,Omit 工具类型的功能是返回去除指定的键值之后返回的新类型,下面我们看一个具体的示例:
1 2 3 4 5 6 7 type Omit <T, K extends keyof any > = Pick <T, Exclude <keyof T, K>>type NewPerson = Omit <Person , 'weight' >interface NewPerson { name : string age ?: number }
联合类型 Exclude 在介绍 Omit 类型的实现中,我们使用了 Exclude 类型。通过使用 Exclude 类型,我们从接口的所有属性中去除了指定属性,因此,Exclude 的作用就是从联合类型中去除指定的类型。具体的示例:
1 2 3 4 5 6 7 type Exclude <T, U> = T extends U ? never : Ttype T = Exclude <'a' | 'b' | 'c' , 'a' > type NewPerson = Omit <Person , 'weight' >type NewPerson = Pick <Person , Exclude <keyof Person , 'weight' >>type ExcludeKeys = Exclude <keyof Person , 'weight' >
Extract 类型的作用与 Exclude 正好相反,Extract 主要用来从联合类型中提取指定的类型,类似于操作接口类型中的 Pick 类型。
1 2 type Extract <T, U> = T extends U ? T : never type T = Extract <'a' | 'b' | 'c' , 'a' >
此外,我们还可以基于 Extract 实现一个获取接口类型交集的工具类型,如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Intersect <T, U> = { [K in Extract <keyof T, keyof U>]: T[K] } interface Person { name : string age ?: number weight ?: number } interface NewPerson { name : string age ?: number } type T = Intersect <Person , NewPerson >type T = { name : string age ?: number }
NonNullable NonNullable 的作用是从联合类型中去除 null 或者 undefined 的类型。
1 2 3 4 type NonNullable <T> = T extends null | undefined ? never : Ttype NonNullable <T> = Exclude <T, null | undefined >type T = NonNullable <string | number | undefined | null >
Record Record 的作用是生成接口类型,然后我们使用传入的泛型参数分别作为接口类型的属性和值。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Record <K extends keyof any , T> = { [P in K]: T } type MenuKey = 'home' | 'about' | 'more' interface Menu { label : string hidden ?: boolean } const menus : Record <MenuKey , Menu > = { about : { label : '关于' }, home : { label : '主页' }, more : { label : '更多' , hidden : true }, }
在 TypeScript 中,keyof any 指代可以作为对象键的属性,如下示例:
函数类型 ConstructorParameters ConstructorParameters 可以用来获取构造函数的构造参数,而 ConstructorParameters 类型的实现则需要使用 infer 关键字推断构造参数的类型。
关于 infer 关键字,我们可以把它当成简单的模式匹配来看待。如果真实的参数类型和 infer 匹配的一致,那么就返回匹配到的这个类型。
1 2 3 4 5 6 type ConstructorParameters <T extends new (...args : any ) => any > = T extends new (...args : infer P) => any ? P : never class Person { constructor (name : string , age ?: number ) {} } type T = ConstructorParameters <typeof Person >
Parameters Parameters 的作用与 ConstructorParameters 类似,Parameters 可以用来获取函数的参数并返回序对,如下示例:
1 2 3 4 5 6 7 type Parameters <T extends (...args : any ) => any > = T extends ( ...args : infer P ) => any ? P : never type T0 = Parameters <() => void > type T1 = Parameters <(x : number , y ?: string ) => void >
ReturnType ReturnType 的作用是用来获取函数的返回类型,下面我们看一个具体的示例:
1 2 3 4 5 6 7 type ReturnType <T extends (...args : any ) => any > = T extends ( ...args : any ) => infer R ? R : any type T0 = ReturnType <() => void > type T1 = ReturnType <() => string >
ThisParameterType ThisParameterType 可以用来获取函数的 this 参数类型。
1 2 3 4 type ThisParameterType <T> = T extends (this : infer U, ...args : any []) => any ? U : unknown type T = ThisParameterType <(this : Number , x : number ) => void >
ThisType ThisType 的作用是可以在对象字面量中指定 this 的类型。ThisType 不返回转换后的类型,而是通过 ThisType 的泛型参数指定 this 的类型,下面看一个具体的示例:
注意:如果你想使用这个工具类型,那么需要开启 noImplicitThis 的 TypeScript 配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type ObjectDescriptor <D, M> = { data ?: D methods ?: M & ThisType <D & M> } function makeObject<D, M>(desc : ObjectDescriptor <D, M>): D & M { let data : object = desc.data || {} let methods : object = desc.methods || {} return { ...data, ...methods } as D & M } const obj = makeObject ({ data : { x : 0 , y : 0 }, methods : { moveBy (dx : number , dy : number ) { this .x += dx this .y += dy }, }, }) obj.x = 10 obj.y = 20 obj.moveBy (5 , 5 )
OmitThisParameter OmitThisParameter 工具类型主要用来去除函数类型中的 this 类型。如果传入的函数类型没有显式声明 this 类型,那么返回的仍是原来的函数类型。
1 2 3 4 5 6 type OmitThisParameter <T> = unknown extends ThisParameterType <T> ? T : T extends (...args : infer A) => infer R ? (...args : A ) => R : T type T = OmitThisParameter <(this : Number , x : number ) => string >
字符串类型 模板字符串 TypeScript 自 4.1 版本起开始支持模板字符串字面量类型。为此,TypeScript 也提供了 Uppercase、Lowercase、Capitalize、Uncapitalize 这 4 种内置的操作字符串的类型,如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 type Uppercase <S extends string > = intrinsictype Lowercase <S extends string > = intrinsictype Capitalize <S extends string > = intrinsictype Uncapitalize <S extends string > = intrinsictype T0 = Uppercase <'Hello' > type T1 = Lowercase <T0 > type T2 = Capitalize <T1 > type T3 = Uncapitalize <T2 >
在上述示例中,这 4 种操作字符串字面量工具类型的实现都是使用 JavaScript 运行时的字符串操作函数计算出来的,且不支持语言区域设置。以下代码是这 4 种字符串工具类型的实际实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 function applyStringMapping (symbol : Symbol , str : string ) { switch (intrinsicTypeKinds.get (symbol .escapedName as string )) { case IntrinsicTypeKind .Uppercase : return str.toUpperCase () case IntrinsicTypeKind .Lowercase : return str.toLowerCase () case IntrinsicTypeKind .Capitalize : return str.charAt (0 ).toUpperCase () + str.slice (1 ) case IntrinsicTypeKind .Uncapitalize : return str.charAt (0 ).toLowerCase () + str.slice (1 ) } return str }
在上述代码中可以看到,字符串的转换使用了 JavaScript 中字符串的 toUpperCase 和 toLowerCase 方法,而不是 toLocaleUpperCase 和 toLocaleLowerCase。其中 toUpperCase 和 toLowerCase 采用的是 Unicode 编码默认的大小写转换规则。
类型编程:如何打造属于自己的工具类型? 类型物料 在正式造轮子之前,我们先来熟悉一下即将用到的物料,这可能涉及前面每一讲中的知识点和一些新语法。
泛型 首先是泛型(回顾 10 讲),笔者认为工具类型的本质就是构造复杂类型的泛型。如果一个工具类型不能接受类型入参,那么它和普通的类型别名又有什么区别?因此,使用泛型进行变量抽离、逻辑封装其实就是在造类型的轮子,下面举一个具体的示例:
1 2 type isXX = 1 extends number ? true : false type isYY = 'string' extends string ? true : false
这明显是一种效率低下的做法,因为我们不能把其中的逻辑复用在对其他类型子类型关系的判断上。这时,我们就需要把确切的类型抽离为入参,然后封装成一个可复用的泛型。
1 2 3 4 type isSubTying<Child , Par > = Child extends Par ? true : false type isXX2 = isSubTyping<1 , number > type isYY2 = isSubTyping<'string' , string > type isZZ2 = isSubTyping<true , boolean >
条件类型 如我们在泛型中提到,TypeScript 支持使用三元运算的条件类型,它可以根据 ?前面的条件判断返回不同的类型。同时,三元运算还支持嵌套。
在三元运算的条件判断逻辑中,它主要使用 extends 关键字判断两个类型的子类型关系,如下代码所示:
1 2 3 4 5 type isSubTyping<Child , Par > = Child extends Par ? true : false type isAssertable<T, S> = T extends S ? true : S extends T ? true : false type isNumAssertable = isAssertable<1 , number > type isStrAssertable = isAssertable<string , 'string' > type isNotAssertable = isAssertable<1 , boolean >
分配条件类型(Distributive Conditional Types) 在条件类型中,如果入参是联合类型,则会被拆解为一个个独立的(原子)类型(成员),然后再进行类型运算。
1 2 3 4 5 6 type BooleanOrString = string | boolean type StringOrNumberArray <E> = E extends string | number ? E[] : Etype WhatIsThis = StringOrNumberArray <BooleanOrString > type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString [] : BooleanOrString
同样,通过某些手段强制类型入参被当成一个整体,也可以解除类型分配,如下示例:
1 2 type StringOrNumberArray <E> = [E] extends [string | number ] ? E[] : Etype WhatIsThis = StringOrNumberArray <string | boolean >
注意:包含条件类型的泛型接收 never 作为泛型入参时,存在一定“陷阱”
1 2 3 4 5 6 type GetSNums = never extends number ? number [] : never extends string ? string [] : never type GetNever = StringOrNumberArray <never >
泛型 StringOrNumberArray 的实现与示例第 1 行“=”右侧的逻辑并没有任何区别(除 never 被抽离成入参之外)。这是因为 never 是不能分配的底层类型,如果作为入参以原子形式出现在条件判断 extends 关键字左侧,则实例化得到的类型也是 never。
1 2 3 4 5 6 7 8 type UsefulNeverX <T> = T extends {} ? T[] : []type UselessNeverX <T, S> = S extends {} ? S[] : []type UselessNeverY <T, S> = S extends {} ? T[] : []type UselessNeverZ <T> = [T] extends [{}] ? T[] : []type ThisIsNeverX = UsefulNeverX <never > type ThisIsNotNeverX = UselessNeverX <never , string > type ThisIsNotNeverY = UselessNeverY <never , string > type ThisIsNotNeverZ = UselessNeverZ <never >
条件类型中的类型推断 infer 另外,我们可以在条件类型中使用类型推断操作符 infer 来获取类型入参的组成部分,比如说获取数组类型入参里元素的类型。
1 2 3 type ElementTypeOfArray <T> = T extends (infer E)[] ? E : never type isNumber = ElementTypeOfArray <number []> type isNever = ElementTypeOfArray <number >
我们还可以通过 infer 创建任意个类型推断参数,以此获取任意的成员类型,如下示例:
1 2 3 4 5 type ElementTypeOfObj <T> = T extends { name : infer E; id : infer I } ? [E, I] : never type isArray = ElementTypeOfObj <{ name : 'name' ; id : 1 ; age : 30 }> type isNever = ElementTypeOfObj <number >
索引访问类型 索引访问类型其实更像是获取物料的方式,首先我们可以通过属性名、索引、索引签名按需提取对象(接口类型)任意成员的类型(注意:只能使用 [索引名] 的语法 ),如下示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface MixedObject { animal : { type : 'animal' | 'dog' | 'cat' age : number } [name : number ]: { type : string age : number nickname : string } [name : string ]: { type : string age : number } } type animal = MixedObject ['animal' ]type animalType = MixedObject ['animal' ]['type' ]type numberIndex = MixedObject [number ]type numberIndex0 = MixedObject [0 ]type stringIndex = MixedObject [string ]type stringIndex0 = MixedObject ['string' ]
在示例的第 16 行,我们通过 ‘animal’ 索引获取了 MixedObject 接口的 animal 属性的类型。在第 17 行,我们通过多级属性索引获取了更深层级 type 属性的类型。
然后,在第 18 行、第 19 行,我们通过 number 类型索引签名和数字索引 0 获取了第 6~10 行定义的同一个接口类型。
最后,在第 20 行、第 21 行,我们通过 string 类型索引签名和字符串字面量索引 ‘string’ 获取了第 11~14 行定义的同一个接口类型。
keyof 可以使用 keyof 关键字提取对象属性名、索引名、索引签名的类型
1 2 3 type MixedObjectKeys = keyof MixedObject type animalKeys = keyof animal type numberIndexKeys = keyof numberIndex
typeof 如果我们在表达式上下文中使用 typeof,则是用来获取表达式值的类型,如果在类型上下文中使用,则是用来获取变量或者属性的类型。当然,在 TypeScript 中,typeof 的主要用途是在类型上下文中获取变量或者属性的类型,下面我们通过一个具体示例来理解一下。
1 2 3 4 let StrA = 'a' const unions = typeof StrA const str : typeof StrA = 'string' type DerivedFromStrA = typeof StrA
对于任何未显式添加类型注解或值与类型注解一体(比如函数、类)的变量或属性,我们都可以使用 typeof 提取它们的类型,这是一个十分方便、有用的设计,如下示例:
1 2 3 4 5 6 7 const animal = { id : 1 , name : 'animal' , } type Animal = typeof animalconst animalFun = ( ) => animaltype AnimalFun = typeof animalFun
映射类型 我们可以使用索引签名语法和 in 关键字限定对象属性的范围,如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 type SpecifiedKeys = 'id' | 'name' type TargetType = { [key in SpecifiedKeys ]: any } type TargetGeneric <O extends string | number | symbol > = { [key in O]: any } type TargetInstance = TargetGeneric <SpecifiedKeys > interface ITargetInterface { [key in SpecifiedKeys ]: any }
注意:我们只能在类型别名定义中使用 in,如果在接口中使用,则会提示一个 ts(1169) 的错误
在定义类型时,我们可以组合使用 in 和 keyof,并基于已有的类型创建一个新类型,使得新类型与已有类型保持一致的只读、可选特性,这样的泛型被称之为映射类型。
注意:in 和 keyof 也只能在类型别名定义中组合使用。
1 2 3 4 5 6 7 8 9 10 11 interface SourceInterface { readonly id : number name ?: string } type TargetType = { [key in keyof SourceInterface ]: SourceInterface [key] } type TargetGenericType <S> = { [key in keyof S]: S[key] } type TargetInstance = TargetGenericType <SourceInterface >
同样,我们可以在映射类型中使用 readonly、? 修饰符来描述属性的可读性、可选性,也可以在修饰符前添加 +、- 前缀表示添加、移除指定修饰符(默认是 +、添加),如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type TargetGenericTypeReadonly <S> = { readonly [key in keyof S]: S[key] } type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly <SourceInterface > type TargetGenericTypeOptional <S> = { [key in keyof S]?: S[key] } type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional <SourceInterface > type TargetGenericTypeRemoveReadonly <S> = { -readonly [key in keyof S]: S[key] } type TargetGenericTypeRemoveReadonlyInstance = TargetGenericTypeRemoveReadonly <SourceInterface > type TargetGenericTypeRemoveOptional <S> = { [key in keyof S]-?: S[key] } type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOptional <SourceInterface >
使用 as 重新映射 key 自 TypeScript 4.1 起,我们可以在映射类型的索引签名中使用类型断言
1 2 3 4 5 type TargetGenericTypeAssertiony <S> = { [key in keyof S as Exclude <key, 'id' >]: S[key] } type TargetGenericTypeAssertionyInstance = TargetGenericTypeAssertiony <SourceInterface >
造轮子 ReturnTypeOfResolved ReturnTypeOfResolved 和官方 ReturnType 的区别:如果入参 F 的返回类型是泛型 Promise 的实例,则返回 Promise 接收的入参。
我们可以借鉴 ReturnType 的定义实现 ReturnTypeOfResolved,如下示例:
1 2 3 4 5 6 7 8 type ReturnTypeOfResolved <F extends (...args : any ) => any > = F extends ( ...args : any [] ) => Promise <infer R> ? R : ReturnType <F> type isNumber = ReturnTypeOfResolved <() => number > type isString = ReturnTypeOfResolved <() => Promise <string >>
Merge 基于映射类型将类型入参 A 和 B 合并为一个类型的泛型 Merge<A, B>
1 2 3 4 5 6 7 8 9 10 11 type Merge <A, B> = { [key in keyof A | keyof B]: key extends keyof A ? key extends keyof B ? A[key] | B[key] : A[key] : key extends keyof B ? B[key] : never } type Merged = Merge <{ id : number ; name : string }, { id : string ; age : number }>
Equal 再来实现一个自定义工具类型 Equal<S, T>
,它可以用来判断入参 S 和 T 是否是相同的类型。如果相同,则返回布尔字面量类型 true,否则返回 false。
此时,我们很容易想到,如果 S 是 T 的子类型且 T 也是 S 的子类型,则说明 S 和 T 是相同的类型,所以 Equal 的实现似乎是这样的:
1 2 3 type EqualV1 <S, T> = S extends T ? (T extends S ? true : false ) : false type ExampleV11 = EqualV1 <1 | (number & {}), number > type ExampleV12 = EqualV1 <never , never >
在示例中的第 1 行,我们实现了泛型 EqualV1;第 2 行中的第一个入参是联合类型,因为分配条件类型的设定,所以第一个类型入参被拆解,最终返回类型 boolean(实际上是联合类型 true | false)。同样,在第 3 行中,当入参是 never,则返回类型 never。因此,EqualV1 并不符合我们的预期。
此时,我们需要使用 [] 解除条件分配类型和 never “陷阱”,确保自定义泛型仅返回 true 或者 false,所以前面示例的改良版本 EqualV2 如下:
1 2 3 4 type EqualV2 <S, T> = [S] extends [T] ? ([T] extends [S] ? true : false ) : false type ExampleV21 = EqualV2 <1 | (number & {}), number > type ExampleV22 = EqualV2 <never , never > type ExampleV23 = EqualV2 <any , number >
在示例中的第 2 行、第 3 行,虽然我们解决了联合类型和 never 的问题,但是还是无法区分万金油类型 any 和其他类型。在第 4 行,当入参是 any 和 number,预期应该返回 false,却返回了 true。
这时,我们还需要使用一个可以能识别 any 的改良版 EqualV3 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type IsAny <T> = 0 extends 1 & T ? true : false type EqualV3 <S, T> = IsAny <S> extends true ? IsAny <T> extends true ? true : false : IsAny <T> extends true ? false : [S] extends [T] ? [T] extends [S] ? true : false : false type ExampleV31 = EqualV3 <1 | (number & {}), number > type ExampleV32 = EqualV3 <never , never > type ExampleV34 = EqualV3 <any , any > type ExampleV33 = EqualV3 <any , number > type ExampleV35 = EqualV3 <never , any >
思考题:尝试找一个 ExampleV3 不能正确分区类型的反例,并改良 ExampleV3。