TypeScript 高级教程 – TypeScript 类型体操 (第三篇)

前言

在 第一部 – 把 TypeScript 当强类型语言使用 和 第二部 – 把 TypeScript 当编程语言使用 后, 我们几乎已经把 TypeScript 的招数学完了.

第三部就要开始做练习题了, 这样才能融会贯通.

记得, 做练习题只是单纯为了更理解 TypeScript 这门语言. 在简单的项目中是没有必要的. 我们可以使用各种 Ultility 库 (e.g. type-festts-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 ObjectIncludesPop

Deep ReadonlyChainable OptionsPromise.all

PermutationKebabCaseAnyOf

IsUnionReplaceKeysRemove Index Signature

Percentage ParserMinusOneTuple to Nested Object

BEM style stringAllCombinations

 

posted @ 2022-11-19 13:01  兴杰  阅读(525)  评论(0编辑  收藏  举报