TypeScript 高级教程 – TypeScript 类型体操 (第三篇)
前言
在 第一部 – 把 TypeScript 当强类型语言使用 和 第二部 – 把 TypeScript 当编程语言使用 后, 我们几乎已经把 TypeScript 的招数学完了.
第三部就要开始做练习题了, 这样才能融会贯通.
记得, 做练习题只是单纯为了更理解 TypeScript 这门语言. 在简单的项目中是没有必要的. 我们可以使用各种 Ultility 库 (e.g. type-fest, ts-toolbelt) 来帮助我们完成业务代码.
type-challenges
社区已经提供了各种题目. 项目 : Github – type-challenges
一共分 4 个等级, 全部撸一遍就可以了
选择题目后会看见
然后开始做题, 会进入到 TypeScript Playground
上半段是题目讲解, 中间是我们回答的地方, 下面是测试代码, 答案错误它会显示红线, 正确则红线消失
做题
小技巧与知识点
这里记入一些常用的技巧和知识点
1. Tuple to Union
type Tuple1 = [string, number]; type Union1 = Tuple1[number]; // string | number
2. Looping and Return by 递归
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest] ? Equal<U, First> extends true ? true : Includes<Rest, U> : false;
通过 extends + infer + rest + recursive 可以 loop Tuple 返回一个 Type.
3. Keyof "able"
在解 Deep Readonly 题时, 里面用到了一个 keyof able 技巧
type DeepReadonly<T> = { readonly [P in keyof T] : keyof T[P] extends never ? T[P] : DeepReadonly<T[P]> }
我们经常会有误区, 认为 keyof albe 就相等于 object. 但其实函数也是 object. 所以最正确的判断应该是 keyof able.
当 keyof SomeType 返回 never 就表示这个类型是不能被 keyof 的, 也就 not keyof able 了.
4. Infer Tuple Types from Array Values
在解 Promise.all 题时, 里面用到了一个 [...T] 的技巧
type MapAwaited<TArray> = { [Index in keyof TArray] : Awaited<TArray[Index]> }; declare function PromiseAll<T extends readonly unknown[]>(values: readonly [...T]): Promise<MapAwaited<T>>;
它的作用有点类型 infer values 里面的类型, 变成 Tuple. 注意是 Tuple 而不是 Array 哦.
5. Template Literal extends + never = always false
在解 Replace 题时, 里面用到了一个 extends + never = skip 的技巧
type Replace<S extends string, From extends string, To extends string> = S extends `${infer First}${ From extends '' ? never : From }${infer Last}` ? `${First}${To}${Last}` : S; Expect<Equal<Replace<'foobarbar', '', 'foo'>, 'foobarbar'>>,
当 From = empty string 时, 我们想跳过, 那么就可以利用 never
type Result2 = 'Test' extends `Test${''}` ? true : false; // true 如果是 emtpty string 是可以匹配到的 type Result1 = 'Test' extends `Test${never}` ? true : false; // false 而用 never 就匹配不到了
6. extends any
经常会看见
T extends any ? "SomeTypes..." : never
这句的目的是强制把一个类型转换成另一个类型, T extends any 100% 是 true.
Union to Intersection 就用到了这个技巧.
7. Filter Object Keys by Old School Way
因为这个 old school way 用到比较多招数, 所以特别介绍一下. new way 用 Map + as never 就可以了.
type FunctionOnlyKeys<T> = { [K in keyof T] : T[K] extends Function ? K : never }[keyof T] type Obj = { getStr : () => string; str : string; getNum : () => number; num : number }; type Keys = FunctionOnlyKeys<Obj>; // 'getStr' | 'getNum' type Obj1 = { [K in Keys] : Obj[K] }
最关键的是这一句
type FunctionOnlyKeys<T> = { [K in keyof T] : T[K] extends Function ? K : never }[keyof T]
1. 首先通过 Mapped 做出对象, 这个对象拥有所有的 keys, value 如果是 Function 那就转换成 keyName 如果不是 Function 那就转换成 never.
2. 然后通过 Indexed Access Types obj[keyof T] 获取 value, 由于 keyof T 是 Union 它表示所有的 keys, 于是它会获取到所有的 values 以 Union 形成呈现.
3. 这个 Union 里面就有 keyName 和 never, 而 never 在 Union 里会被移除, 于是最终就只留下了 value 是 Function 的 keyName. 这就间接达到了 filter keys 的作用了.
8. 当 keyof 遇到 Union
type Obj = { str: string } | { str: string, num : number }; type KeyofObj = keyof Obj; // "str"
最终结果只会有每个对象共同拥有的 Key.
如果希望获取到所以的 Keys, 可以这样写
参考: Stack Overflow – Intersection of mapped types
type MyKeyof<T> = T extends Record<infer K, any> ? K : never; type MyKyeofObj = MyKeyof<Obj>; // str, num
9. [T] extends [never]
[T] extends [never] 的作用是为了避开 never extends whatever = never
通常它会和递归, Exclude 一起使用, 比如
Permutation 这一题
type Permutation<T, K=T> = [T] extends [never] ? [] : K extends K ? [K, ...Permutation<Exclude<T, K>>] : never type Permuted = Permutation<'a' | 'b'> // ['a', 'b'] | ['b' | 'a']
10. 当 Tuple Rest 遇上 Union
type A = ['a'] | ['b']; type B = [...A]; // 可能你以为会是: ['a' | 'b'] or [['a'] | ['b']] // 但其实是: ['a'] | ['b'];
当 Tuple Rest 遇上 Union 会有点 flatmap 的效果.
这个技巧在 Permutation 题会看见.
11. extends Object 判断对象
type R = 1 extends {} ? true : false; // true
答案是 true, 1 是 object, 但通常我们认为的 object 是 key value pair 那种
所以下面这个才是正确匹配方式
type r1 = 1 extends { [K in keyof any] : any } ? true : false; // false type r2 = {} extends { [K in keyof any] : any } ? true : false; // true type r3 = { str: string } extends { [K in keyof any] : any } ? true : false; // true
有一点要注意, 不管 object 是否含有 key 都是 true. 如果我们想匹配 empty object 的话, 需要写一个 never
type r2 = {} extends { [K in keyof any] : never } ? true : false; // true type r3 = { str: string } extends { [K in keyof any] : never } ? true : false; // false type r4 = {} extends Record<PropertyKey, never> ? true : false; // 用 Record 配 PropertyKey 也是一样的效果
12. declare variable on generic
在解 IsUnion 时, 答案开头有一个 U = T
type IsUnion<T, U = T> = [T] extends [never] ? false : T extends any ? [U] extends [T] ? false : true : never;
这招的作用是开变量, 让内部可以使用. 你可以 U = T, U = Whatever<T> 各种方式去定义, transform 这个变量来使用哦.
13. Equal
TS 只有 extends 没有 equal. 在解 Remove Index Signature 题时需要判断 Object Key 是不是 string | number | symbol
type A<T> = { [K in keyof T as K extends string ? never : K] : any }
上面这样写是错误的, 因为任何 String Literal 都是 "一种" string, 比如 'name' extends string = true
要想判断是不是真的 string, 一个巧思是反过来匹配. string extends 'name' = false;
type A<T> = { [K in keyof T as string extends K ? never : K] : any } type r = A<{ 'str' : any } & { [key: string] : any }>; // { str: any }
于是像上面这样就可以移除 string key 了.
还有一种更正规的方法实现 Equal, 下面会教.
13. Flatten Object
在解 PartialByKeys 时发现了 Equal 和 extends 对 Intersection Object 的区别对待
type Obj1 = { str: string } & { num: number } type Obj2 = { str: string, num : number} type Same1 = Obj1 extends Obj2 ? true : false; // true type Same2 = Obj2 extends Obj1 ? true : false; // true type Same3 = Equal<Obj1, Obj2>; // false
显然 Equal 更加严格. 那怎么办呢? 这时就需要把 Intersection Object 变成一个 Object
type Flatten<T> = { [P in keyof T]: T[P] }
非常简单, 当 keyof 遇上 Intersection Object 会把所有的 Keys 拿出来. 这样就 flat 了
14. String to Union
type StringToUnion<S extends string> = S extends `${infer First}${infer Rest}` ? First | StringToUnion<Rest> : never;
其实很简单...其实很自然...
高级技巧
1. Equal
在 type-challenges 的测试代码中, 用到了一个叫 Equal 的 Ultility.
我们之前有讲过, TypeScript 只有 extends 没有 equal. 比如下面这题
type Yes = { age: 11 } extends {} ? true : false; // true
因为 { age: 11 } 是 "一种" {} 所以结果是 true
那如果我们想要 "Equal" 呢?
可以这么写
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
我是没看懂的. 有兴趣的朋友可以看这里 Github – [Feature request]type level equal operator #27024
大致意思是, 当 conditional 遇上 泛型 T 产生了化学反应, 所以就有了 equal 的功能.
2. Union to Intersection
参考: Stack Overflow – Transform union type to intersection type
首先要知道, 想输出 Intersection 只有一个办法, 那就是使用 multiple infer + 逆变位置
之前在介绍 multiple infer 就有提过这个技巧.
最终代码是这样的
type UnionToIntersection<U> = ( U extends any ? (p: U) => void : never ) extends (p: infer I) => void ? I : never; type r1 = UnionToIntersection<{ str: string } | { num: number}>; // { str: string } & { num: number }
第一步是
U extends any ? (p: U) => void : never
U extends any 一定是 true, 所以一定会返回 (p: U) = void
这句的目的是把传入的 Union Tpyes 放入到参数这个位置, 因为我们需要逆变位置, 而参数位置正是逆变位置,
U 是 Union 所以这句最终会变成 (p: U1) => void | (p: U2) => void | ...
接着再 infer 出来
extends (p: infer I) => void
相等于
type r1 = ( ((p: { str: string }) => void) | ((p : { num: number }) => void) ) extends (p : infer P) => void ? P : never;
Union Type extends ...infer + 逆变位置, 最终输出了 Intersection Type.
提醒
在 TypeScript v3.6 以后, string | number 会返回 never 而不是 string & number
type r1 = UnionToIntersection<string | number>; // never
这个是正常的, 因为 string & number 是不可能发生的. 所以就返回了 never.
所以其实它是 string | number -> string & number -> never.
3. Union to Tuple
参考: Stack Overflow – How to transform union type to tuple type
这个是最终答案 (来自上面参考链接)
// oh boy don't do this type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never // TS4.0+ type Push<T extends any[], V> = [...T, V]; // TS4.1+ type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L> type abc = 'a' | 'b' | 'c'; type t = TuplifyUnion<abc>; // ["a", "b", "c"]
我们一个一个讲解
1. 首先是 Union to Intersection, 这个上面介绍过了.
2. LastOf<Union>
type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never;
它的功能是, 输入一个 Union, 它会把最后一个类型取出来
type R1 = LastOf<'a' | 'b' | 'c'>; // "c" type R2 = LastOf<string | number>; // number type R3 = LastOf<number | string>; // number 翻车 type R4 = LastOf<number | boolean>; // boolean 翻车
最后 2 个翻车了. 原因是 Union 本来就不可能被正确的 for loop. 它是没有 order 概念的. 这也是为什么答主强调, 不要有 Union to Tuple 这种思想, Github 也有 Issue 说到这点.
而我介绍这个主要是分享它的实现技巧.
假设 T = 'a' | 'b' | 'c'
UnionToIntersection<T extends any ? () => T : never>
上面这句的输入是 Union () => 'a' | () => 'b' | () => 'c' (使用的技巧是 Distributive Conditional Types), 返回结果是 Intersection () => 'a' & () => 'b' & () => 'c'
接着
extends () => (infer R) ? R : never;
上一个 part 返回的结果是 intersection function 也等同于 function overload, 而 function overload 配上 infer 会拿到最后一个 function 的 return. 这里的技巧是当 Infer 遇上 Function Overload
所以最终结果是 'c'
接着
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>
[T] extends [never] 的作用是为了避开 never extends whatever = never
它是一个递归函数, 主要作用是获取 LastOf Union 然后 Push to Tuple
判断 N 是否为 true 来终止递归.
题目解析
这里列举一些有特殊的题目来讲讲
1. Easy – Includes
第一题把我难到的是 includes (它的 level 是 easy...)
题目是实现 array.includes 功能
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
第二个参数在 array 内就返回 true, 否则 false.
错误的思路
我第一个想到的解法是
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;
把 Tuple 换成 Union 然后 extends, 这招只能 cover 某些场景
因为 { a: 'A' } extends {} 是 true. 而这里要的是 equal 而不是 extends
正解
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest] ? Equal<U, First> extends true ? true : Includes<Rest, U> : false;
里头用了几个技巧.
1. looping Tuple, 它是通过 extends + infer + rest + recursive 来实现的
2. Equal Utility, 这个是 type-challenges 提供的 Utility
2. Medium – Trim Left
针对 String Literal 的函数, 功能是除去左边的空格
" Hello World " -> "Hello World "
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World '
解答
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer Rest}` ? TrimLeft<Rest> : S
几个点注意
1. 只要涉及修改 String Literal, 那么 extends Template Literal + infer 是一定会用的招数
2. infer Rest 不需要 dotdotdot ... 哦, 这个和处理 Tuple 不同
3. 利用递归实现从左到右的扫描
3. Medium – Permutation
参考答案和解释: Permutation (with explanations)
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
几个难题和思路
1. 返回的 Union 数目比输入的多. 这就不能只用 Distributive Conditional Types. 要配上 Tuple Rest Union 才可以.
2. 笛卡尔积需要用递归来实现. 每次缩小 Union
首先是
type Permutation<FullUnion, SingleUnion = FullUnion> = SingleUnion extends any ? [SingleUnion, ...Permutation<Exclude<FullUnion, SingleUnion>>] : never;
SingleUnion = FullUnion 复制一个 Union 出来 loop. 另一个保留当完整 Union 使用
SingleUnion extends any 做一个 for loop 提取单个 SingleUnion, 后续的用 Exclude 把当前 SingleUnion 从完整 Union 过滤掉.
'A' | 'B' | 'C' 就变成 [A, ...递归('B' | 'C')] 这样.
[SingleUnion, ...Permutation<Exclude<FullUnion, SingleUnion>>]
这一句用了递归 + Tuple Rest Union. rest union 有 flatmap 的效果, 会把整个 [SingleUnion, ...递归] duplicate 出来.
所以本来 'A' | 'B' | 'C' 应该输出 3 个 Union 但结果输出了 6 个.
有递归就需要一个停止, 最后加上
type Permutation<T, U = T> = [T] extends [never] ? [] : U extends any ? [U, ...Permutation<Exclude<T, U>>] : never;
因为 Exclude 到最后没有 result 会返回 never, 所以通过判断是否是 never 来停止递归. 这个判断还用到了 [T] extends [never] 小技巧 哦.
4. Medium – Diff
题目是把 2 个 Object 相同的 Keys 移除.
下面是我一开始的答案
type Diff<O, O1> = { [K in (Exclude<keyof O, keyof O1> | Exclude<keyof O1, keyof O>)] : K extends keyof O1 ? O1[K] : K extends keyof O ? O[K] : never }
虽然可以实现, 但是不优雅.
更理想的答案是
type Diff<O, O1> = Omit<O & O1, keyof (O | O1)>;
先通过 O & O1 把 2 个对象 combine 成为大对象, 在 Omit 掉相同的 Keys.
这里用了 keyof Union 小技巧 找出 2 个对象相同的 Keys.
5. Medium – IsUnion
判断是否是 Union. 答案是
type IsUnion<SingleUnion, FullUnion = SingleUnion> = [SingleUnion] extends [never] ? false : SingleUnion extends any ? [FullUnion] extends [SingleUnion] ? false : true : never;
开头的 [SingleUnion] extends [never] 是用来对付 never
接着就是拿 Union 来 for loop 提取出每一个 Union Type, 来和传入的原值做对比.
如果它不是 Union 那么它没有 loop 的效果, 对比结果会是 true, 如果它是 Union, 会有 loop 的效果, 对比变成 Full vs Single 那么结果就是 false.
这样就可以判断出是不是 Union Type 了.
6. Medium – Percentage Parser
题目是 parse string, '+180%' 变成 ['+', '180', '%'], 难点是 +, number, % 都是 Optional, 所以要兼顾各个情况
我一开始的答案是
type PercentageParser<S> = S extends `${infer First extends '+' | '-'}${infer Middle}%` ? [First, Middle, '%'] : S extends `${infer First extends '+' | '-'}${infer Middle}` ? [First, Middle, ''] : S extends `${infer Middle}%` ? ['', Middle, '%'] : S extends `${infer First extends '+' | '-'}` ? [First, '', ''] : S extends `${infer Middle}` ? ['', Middle, ''] : S;
非常粗暴的笛卡尔积 if else if
比较优雅的答案是
type PercentageParser<S, First = ''> = S extends `${infer First extends '+' | '-'}${infer Rest}` ? PercentageParser<Rest, First> : S extends `${infer Middle}%` ? [First, Middle, '%'] : [First, S, ''];
它的思路有点像, reduce + substring 逐个去找, 然后递归传递已经找到的答案,
6. Medium – MinusOne
题目就是 -1
Expect<Equal<MinusOne<55>, 54>>
TS 没有 operator 做加减乘除. 大部分人提供的方案是利用 Tuple 的 length
type MinusOne< T extends number, CurrArray extends any[] = [], NextArray extends any[] = [1, ...CurrArray] > = T extends 0 ? -1 : NextArray['length'] extends T ? CurrArray['length'] : MinusOne<T, NextArray>;
利用递归 + Tuple push 不断增加 Tuple 内容, 最后 compare length 输出.
有特色的题目记入
Tuple to Object, Includes, Pop
Deep Readonly, Chainable Options, Promise.all
IsUnion, ReplaceKeys, Remove Index Signature
Percentage Parser, MinusOne, Tuple to Nested Object
BEM style string, AllCombinations