2023-06-07
TypeScript
00
请注意,本文编写于 563 天前,最后修改于 330 天前,其中某些信息可能已经过时。

目录

类型编程基础
索引类型
映射类型
类型转换(包装 or 拆包)
集合运算
映射类型 是如何复制的?
映射类型面试题
实现一个C类型, 先继承A的所有属性,再继承B的所有属性并将其设置为可选属性(不包括A拥有的)
合并接口,判断符合条件的值是否存在, 不存在就是never
infer
ReturnType、InstanceType、Parameters
更多infer案例
类型体操准备
extends
分布式条件类型特性
判断类型
类型转换技巧
参考资料

类型声明是最好的文档,而类型编程可以类型声明发挥到极致。类型编程是TS高手必备技能,掌握TS编程可以帮助我们的阅读理解框架源码、设计产出自己的类库及文档。本将深入讲解映射类型infer,给出丰满的案例及TS编程题,带你由浅入深的掌握TS类型体操。

类型编程基础

  • TS 类型本身就是一个很复杂的、独立的语言,不仅仅是 JS 的增强和类型注释,类型编程是TS的精髓
  • 学习TS类型编程需要一定的TS功底,函数重载交叉类型泛型索引类型映射类型infer要提前掌握,前三种类型前面的课程已讲述,而索引类型映射类型infer比较偏编程,故意放在这一章节讲解。

索引类型

  • keyof T T的所有字面量的集合
  • 泛型中通常这样写K extends keyof T
    • KT 的所有属性字面量集合
    • T[K]T 的所有属性值字面量集合
ts
interface Person { name: string; age: number; } type Key = keyof Person; // Key 等同于 'name' | 'age' type ObjAddNullByKey<T, K extends keyof T> = { [P in keyof T]: K extends keyof T ? (T[P] | null) : T[P]; } var p: Person = { name: '小花', age: 2, // 这里不能写null } var p1: ObjAddNullByKey<Person, 'age'> = { name: '小花', age: null, // 这里可以写 null }

映射类型

  • 可以非标准粗暴的理解TS类型编程起始于映射类型,从映射类型开始TS类型编程编的有意思了。
  • 映射类型的用途
    • 类型转换(包装 or 拆包) 只读、可选、挑选、复制
    • 集合运算 交差并补

类型转换(包装 or 拆包)

  • Ts内置了一些最基础的泛型工具方法 如: Readonly Partial Required NonNullable
  • 这里类比实现了 Mutable Nullable DeapReadonly
ts
// Person ---> PersonReadonly ---> Person interface Person { name: string; } type MyReadonly<T> = { readonly [P in keyof T]: T[P]; } var zs: MyReadonly<Person> = {name: 'zs'}; // zs.name = 'ls' // 这里报错不让改 // 可变的(即移除readonly) type Mutable<T> = { -readonly [P in keyof T]: T[P] } // typeof zs 取出js对象的数据类型,这里即 MyReadonly<Person> var zs2:Mutable<typeof zs> = zs; zs2.name = 'zs2'; // 类似的还有 Partial Require type MyPartial<T> = { [P in keyof T]?: T[P] | undefined } const ls: MyPartial<Person> = {}; type MyRequired<T> = { [P in keyof T]-?: T[P]; } let ww: MyRequired<typeof ls> = { name: 'ww' // 这里不能注释,必须写name } // NonNullable type NullName = string | null | undefined; let zl: NullName = null; let sunqi: NullName & {} = 'sunq' // sunqi: string let sunqi2: NonNullable<NullName> = 'sunq' // sunqi: string /** * type NonNullable<T> = T & {} * T & {} 如何理解? * 如果是联合类型 比如 `string | null | undefined` 会依次与 `{}`进行 `&` 运算 * string & {} 得到 string * null & {} 得到 never * undefined & {} 得到 never * string | never | never 得到 string */ type Nullable<T> = T | null | undefined; const zhouba: Nullable<string> = null; type DeapReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeapReadonly<T[P]> : T[P] } const wujiu: DeapReadonly<{ name: string, info: { addr: string } }> = { name: 'wujiu', info: { addr: 'shanghai' } } // wujiu.info.addr = 'nanjing' // 这里会报错不让改

集合运算

  • TS内置了 Pick Extract Exclude Omit
  • Merge可以用 | 实现
  • Comple可以用 Exclude 反过来运算
ts
// 挑选 Pick type MyPick<T, K extends keyof T> = { [P in K]: T[P]; } const zs: MyPick<{ name: string, age: number, }, 'name'> = { name: 'zs', // age: 12, // 这里不允许 } // TS内置的交叉并补是针对字面量类型 // 交集 Extract T和U共同的字面量 type MyExtract<T, U> = T extends U ? T : never; const ls: MyExtract<'a'|'b', 'b'> = 'b'; // 只能是'b', 不能写'a' // 差集 Exclude T - U type MyExclude<T, U> = T extends U ? never: T const ww: Exclude<'a' | 'b', 'b'> = 'a'; // 只能是'a', 不能写'b' // 并集 用联合类型实现即可 type Merge<T, U> = T | U; const words: Merge<'a' | 'b', 'b'| 'c'> = 'c' // 也可以是'a'或'b'; // 补集 差集反着来 type Comple<T, U> = U extends T ? never : U const words2: Comple<'a' | 'b', 'b' | 'c'> = 'c' // 只能是 'c' // 踢出Omit Pick的反义词 type MyOmit<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P] } const zl: MyOmit<{ name: string; age: number; sex: 1 | 2 }, 'sex' | 'age'> = { name: 'ls', // age: 23, // 不允许 }

映射类型 是如何复制的?

ts
type Copy<T> = { [P in keyof T]: T[P] } interface Person { readonly name: string; age?: number; sex: 1 | 2; } const p: Copy<Person> = { name: 'zs', sex: 1, } p.age = 12 // p.name = 'ls'; // 无法为“name”赋值,因为它是只读属性。ts(2540)

映射类型面试题

实现一个C类型, 先继承A的所有属性,再继承B的所有属性并将其设置为可选属性(不包括A拥有的)

ts
interface A { name: string; age: number; } interface B { age: number; sex: 1 | 2 } interface C { name: string; age: number; sex?: 1 | 2; } // 要求 C由A和B得到 type C1<A, B> = { [P in keyof A] : A[P]; } & { [P in Exclude<keyof B, keyof A & keyof B>]?: B[P] } const c: C1<A, B> = { name: 'zs', age: 12, } c.sex = 1;

合并接口,判断符合条件的值是否存在, 不存在就是never

ts
// 合并接口,判断符合条件的值是否存在, 不存在就是never interface A { a: string; b: number; } interface B { a: number; b: number; c: boolean; } // C类型是A类型和B类型运算得到 interface C { b: number; } /** * 思路分析: * - A.b与B.b属性值相同,要过滤掉属性b,可以通过映射类型过滤,但是这里的难点是根据值类型过滤 * - 如果是单纯过滤值类型的过来可以根据字面量 {[P in T]: T[P]}[keyof T] * - 我们可以把属性值类型相同的属性作为字面量原封不动的赋值给值,不相同的属性其值赋值为 never 在过滤掉 never, 得到属性字面量集合 */ // 过滤T类型中属性值为U类型的Key type Keys<T, U> = { [P in keyof T]: P extends keyof U ? T[P] extends U[P] ? P : never : never }[keyof T] type AB<A, B> = { [P in Keys<A, B>]: A[P] } var a: AB<A, B> = { // a: 'zs', // 这里报错 b: 23, } // 方法二 type MyExtract<A extends object, B extends object> = { [P in keyof A as P extends keyof B ? A[P] extends B[P]? P : never : never ]: A[P] }

infer

泛型固然强大,但描述多个泛型之间的约束关系有限,通常是表示一对一关系的约束,多个泛型之间的关系参数类型及返回值类型之间的关系无法描述,这里就需要infer(约束类型)登场了。

  • infer 语法 T extends U ? X : Y
    • infer只能在条件类型的extends子句中使用, (必须在entends后面)
    • infer得到的类型只能在true 语句中使用,即X中使用

ReturnTypeInstanceTypeParameters

ts
// ReturnType function getStr(arg: number | boolean | string | object | null | undefined) { if(typeof arg === 'string') { return arg; } if(typeof arg === 'undefined') { return ''; } if(typeof arg === 'object' && !arg) { return ''; } return arg!.toString(); } var a: ReturnType<typeof getStr> = 'abc' // a: string type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; var b: MyReturnType<typeof getStr> = 'ab' // b: string // Parameters 获取参数类型 // type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; // 我们写个不一样的,获取第二个参数的类型 type GetSecondParam<T extends (...args: any) => any> = T extends (first: any, second: infer S, ...rest: any) => any ? S : never; function getSecondParam(a: string, b: number) { console.log(a, b); } const secondType: GetSecondParam<typeof getSecondParam> = 123 // secondType: number // InstanceType 获取实例类型 type MyInstanceType<T extends new (...args: any) => any> = T extends { new (...args): infer R } ? R : any; let obj: MyInstanceType<{ new (...args: any): { name: string; age:number; } }>; // obj: { name: string; age: number; }

更多infer案例

  • 获取数组类型
  • 推断Promise成功值的类型
  • 推断字符串字面量类型的第一个字符对应的字面量类型
  • 推断数组方法。。。
  • 推断字符串方法
TS
// infer可以获取数组类型 type ArrayType<T> = T extends (infer R)[] ? R : any; var arr: ArrayType<number[]>; // arr: number // 获取数组最后一个元素的类型 type ArrayLast<T> = T extends [...infer _, infer Last] ? Last: never; var last: ArrayLast<[string, 's', false]>; // last: false // 获取元组最后一个类型 // 推断Promise成功值的类型 type InferPromise<T> = T extends Promise<infer T> ? T : never; type Resp = InferPromise<Promise<string>>; // Resp: string // 推断字符串字面量类型的第一个字符对应的字面量类型 type InferString<T extends string> = T extends `${infer First}${infer _}` ? First : []; type word = InferString<'First' | 'Second'>; // word: 'F' | 'S' // 综合案例 type Shift<T> = T extends [infer First, ...infer Rest] ? [...Rest] : []; var a1: [number, string] = [1,'2'] ; const result: Shift<[number, string]>[0] = 'str'; // result: string type Pop<T extends any[]> = T extends [...infer L, infer R] ? [...L] : []; type Reverse<T extends unknown[], U extends unknown[] = []> = T extends [] ? U : T extends [infer L, ...infer R] ? Reverse<R, [L, ...U]> : U; type A2 = Reverse<[string, number, boolean]>; // A2: [boolean, number, string] type FlipArguments<T extends Function> = T extends (...arg: infer R) => infer S ? (...arg : Reverse<[...R]>) => S : T; type StartsWith<T extends string, U extends string> = T extends `${U}${infer R}` ? true : false; type TrimLeft<S extends string> = S extends `${infer L}${infer R}` ? L extends ' ' | '\n' | '\t' ? TrimLeft<R> : S : ''; type Trim<S extends string> = S extends `${' ' | '\t' | '\n'}${infer R}` ? Trim<R> : S extends `${infer L}${' ' | '\t' | '\n'}` ? Trim<L> : S; type StringToUnion<T extends string, U = never> = T extends '' ? U : T extends `${infer L}${infer R}` ? StringToUnion<R, U | L> : U; var str: StringToUnion<'abc'> // str: 'a' | 'b' | 'c'

类型体操准备

下一篇将记录TS类型体操刷题,在此之前先总结一下类型编程的知识点及技巧,这样刷题的时候不至于太郁闷。

extends

  • TS泛型通过extends实现条件分支逻辑
  • 也是通过它实现循环递归逻辑(循环要借助递归模拟)
  • 继承的规则是怎样的呢?
    • T extends T VS [T] extends [never]
    • T extends true
ts
type TestExtends<T> = T extends T ? true : false; // 传入基本类型都是true 包括 boolean number string bigInt null undefined void any unknown // 传入 Symbol 得到 true // 传入never得到never 那怎样判断never呢? type isNever<T> = [T] extends [never] ? true : false; type TestExtendsTrue<T> = T extends true ? true: false; // 只有 never 和 true 会得到true 其他都是false

分布式条件类型特性

  • 联合类型使用 extends 会进行展开运算 被称为条件分支类型
ts
type StringOrNumber<T> = T extends string ? string : number; type C = StringOrNumber<'world' | 7>; // 类型为 string | number

判断类型

  • 判断是否是空对象 T extends {[k: string]: never}
  • 判断是否是never [T] extends [never] ? true : false
  • 判断是否是联合类型 TS类型体操 IsUnion
ts
type IsUnion<T, C extends T = T> = ( T extends T ? C extends T ? true : unknown // 联合类型 : never ) extends true ? false : true; type A = unknown extends true ? false : true; // true type B = never extends true ? false : true; // false type C = true extends true ? false : true; // false

类型转换技巧

  • keyof T 获取对象类型key得到字符串字面量类型
  • T[keyof T] 获取对象类型所有Value得到的联合类型
  • [P in keyof T] 映射key
  • 交叉类型编辑器提示不优好,可以使用映射聚合为一个类型
  • {[P in keyof T as P extends U ? P : never ]}key变为never进而实现过滤key的效果,该方式同样可以过滤值类型 具体案例 TS类型体操Readonly2 题目 答案
  • T[number] 获取数组元素的枚举值
  • 联合类型转对象可以用in
  • 给一个对象增加属性,但禁止重复添加,怎么办? 具体案例 Chainable Options 题目 答案
ts
interface Per{ name: string; } type AddProp = <T, K extends string>( obj: T, key: K extends keyof T ? never: K, val: any, ) => any; const addProp: AddProp = function(obj, key, val) { // ... } addProp({name: 'zs'}, 'age', 12); // ok addProp({name: 'zs'}, 'name', 'ls'); // 报错 不能添加重复属性
  • stringnumber 的转换
ts
type ParseInt<T extends string> = T extends `${infer Digit extends number}` ? Digit : never
  • 对象类型转换注意事项
ts
// 类型转换问题 仔细看三种Omit的问题 // 该案例在类型编程中很有意义 type MyOmit1<T, K extends keyof T = keyof T> = { [P in Exclude<keyof T, K>]: T[P]; } type MyOmit2<T, K extends keyof T = keyof T> = { [P in keyof T as P extends K ? never : P]: T[P]; } type MyOmit3<T, K extends keyof T = keyof T> = Pick<T, Exclude<keyof T, K>> interface User { name?: string age?: number // readonly age?: number } type AA = MyOmit1<User, 'name'> // AA: { age: number | undefined; } type BB = MyOmit2<User, 'name'> // BB: { age?: number | undefined; } type CC = MyOmit3<User, 'name'> // BB: { age?: number | undefined; } // 总结, 直接在对象里通过范型函数过滤属性的方式 会丢失属性的 可选、可读 特性

参考资料

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!