前言

在 第一部 – 把 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

TypeScript 高级教程 – TypeScript 类型体操 (第三篇)的更多相关文章

  1. WWF3事件类型活动<第三篇>

    WWF将工作流分为两大类: 面向Human:在工作流运行时通过用户对外部应用程序的操作来影响工作流的业务流转. 面向System:应用程序控制流程. 工作流与应用程序都是可以单独存在的,因此它们之间的 ...

  2. Python高级网络编程系列之第三篇

    在高级篇二中,我们讲解了5中常用的IO模型,理解这些常用的IO模型,对于编写服务器程序有很大的帮助,可以提高我们的并发速度!因为在网络中通信主要的部分就是IO操作.在这一篇当中我们会重点讲解在第二篇当 ...

  3. 深入理解DOM事件类型系列第三篇——变动事件

    × 目录 [1]删除节点 [2]插入节点 [3]特性节点[4]文本节点 前面的话 变动(mutation)事件能在DOM中的某一部分发生变化时给出提示,这类事件非常有用,但都只能使用DOM2级事件处理 ...

  4. Typescript高级类型与泛型难点详解

    最近做的TS分享,到了高级类型这一块.通过琢磨和实验还是挖掘出了一些深层的东西,在此处做一下记录,也分享给各位热爱前端的小伙伴.   其实在学习TS之前就要明确以下几点:   1. typescrip ...

  5. TypeScript 高级类型

    ⒈交叉类型(Intersection Types) 交叉类型是将多个类型合并为一个类型. 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性. 例如, Person &a ...

  6. C# vs TypeScript - 高级类型

    总目录 从C#到TypeScript - 类型 从C#到TypeScript - 高级类型 从C#到TypeScript - 变量 从C#到TypeScript - 接口 从C#到TypeScript ...

  7. 从C#到TypeScript - 高级类型

    C# vs TypeScript - 高级类型 上一篇讲了基础类型,基本上用基础类型足够开发了,不过如果要更高效的开发,还是要看下高级类型,这篇和C#共同点并不多,只是延用这个主题. 联合类型 可以从 ...

  8. TypeScript 高级类型 接口(interface)

    在代码的实现或者调用上能设定一定的限制和规范,就像契约一样.通常,我们把这种契约称为接口. TypeScript的核心原则之一是对值所具有的结构进行类型检查. 有时称为“鸭式辨型法”或“结构性子类型化 ...

  9. TypeScript完全解读(26课时)_2.TypeScript完全解读-基础类型

    2.TypeScript完全解读-基础类型 src下新建example文件夹并新建文件.basic-type.ts.截图中单词拼错了.后需注意一下是basic-type.ts 可以装tslint的插件 ...

  10. TypeScript 入门教程学习笔记

    TypeScript 入门教程学习笔记 1. 数据类型定义 类型 实例 说明 Number let num: number = 1; 基本类型 String let myName: string = ...

随机推荐

  1. 10 pdf分享失败

    PC端分享pdf,复制粘贴pdf链接后跳转搜索首页

  2. Bootstrip HTML 查询搜索常用格式模版

    Bootstrip HTML 查询搜索常用格式模版 <form class="form-inline my-3 d-flex align-items-center justify-co ...

  3. 最强AI语音克隆和文本配音工具!与真人无异,CosyVoice下载介绍

    CosyVoice是一个大规模预训练语言模型,深度融合文本理解和语音生成的一项新型语音合成技术,能够精准解析并诠释各类文本内容,将其转化为宛如真人般的自然语音 CosyVoice采用了总共超15万小时 ...

  4. memset函数&&bzro函数_C

    // Code file created by C Code Develop #include "ccd.h" #include "stdio.h" #incl ...

  5. Python 按规则解析字符串中的嵌套函数并实现函数调用

    按规则解析字符串中的嵌套函数并实现函数调用 需求 1.按照一定规则解析字符串中的函数表达式,并替换这些表达式.这些函数表达式可能包含其它函数表达式,即支持函数嵌套 2.函数表达式格式:${ __函数名 ...

  6. 使用update-alternatives管理GCC版本

    目录 简介 操作过程 简介 当操作系统中存在多个版本的GCC时,可以使用使用update-alternatives管理默认使用的编译器版本. 本文使用gcc-9和gcc-11做演示,操作系统为ubun ...

  7. Java--普通方法重载

    [转载自本科老师上课课件] 调用一个重载过的方法时,Java编译程序是如何确定究竟应该调用哪一个方法?以下代码定义了三个重载方法: public void f(char ch){ System.out ...

  8. 【JavaScript】从N个下拉动态监听改变的option值

    同事因为这个问题人傻了,是从Ajax请求获取的动态数据遍历的表格 然后表格行的单元格又有下拉选择,有N个下拉,要取出选择的值进行二次请求 <select name="A" i ...

  9. 【PostgreSQL】01 环境搭建

    [PostgreSQL数据库安装] 数据库本体就没下本机了,直接挂服务器的Docker上面跑 docker pull postgres:9.4 创建容器并运行: docker run --name p ...

  10. maven配置阿里云镜像与修改默认仓库地址

    1.背景 通常来说maven的默认镜像很慢,我们需要一个国内镜像,拉取jar包的时候从国内下载, 当然阿里云镜像是很好的一个候选 2.安装 官网下载一个maven解压即可使用 3.修改配置 第一步,找 ...