近期用了一定时间通读了 type-challenges,感慨 Typescript 也可以写的如此复杂之余,也出于以下几个原因,对题目做了整理,形成本文:
- type-challenges 整体题目较多,而且很多时候我们只是学习为主,并不是以“做题”为目的,这样我们在列表-问题-答案列表-答案详情中跳来跳去很麻烦,也很难甄别优质解答。
- type-challenges 仓库中某些题目,实属偏门,可能对于大多数业务开发来说,永远也用不到,投入时间在这部分,ROI 就会非常低。
- type-challenges 有些题目比较类似,但是却放到了不同的地方,甚至难度不同,笔者认为结合起来一起看可能更高效。
因此,本文对笔者认为重要的、有业务使用场景或者可以给我们以很大启发的题目进行罗列和解析,同时规避了一些复杂度很高,但实际大多数人用不到的题目(比如 Typescript 实现 JSON 解析),对于普通的 Typescript 开发者而言,看完本文的题目就基本能够以一个比较小的代价掌握 type-challenges 中贴近日常开发和业务的部分。
感谢 type-challenges 仓库的贡献者们
Typescript 内置工具函数
实际上,Typescript 自身除了定义类型以外,自己内置了很多非常有用的工具函数,这部分是我们日常 Typescript 开发应当必须掌握,信手拈来的,我建议如果对这部分不熟悉的话,先多读几遍这部分。
这部分内容,可以在这里 了解,也可以在自己的项目中,点进 node_modules/typescript/lib/lib.es5.d.ts
直接了解,注释比较详尽。
实际上,type-challenges 的一部分简单和中等的题目就是实现这些内置工具函数的二次实现,这部分内容我在本文基本没有重复罗列,但是建议大家去阅读官方实现,一般来说也都比较短小,容易理解
type-challenges 部分重点题目
数组第一个元素
使用:
1 | type arr1 = ['a', 'b', 'c'] |
实现:
1 | export type First<T extends any[]> = T extends [infer first, ...any[]] |
除了本题目,还有许多其他类似的实现::
1 | // 数组最后一个元素: |
技巧提示:
- 通过 extends 加三目运算符,完成条件判断。
- 通过类似
infer first, ...any[]
或者[infer First, ...infer Rest]
这种方式来展开数组类型。 - 另外我们还可以使用
[...T, U]
来扩充数组的类型定义。
Tuple to Union
使用:
1 | type Arr = ['1', '2', '3'] |
实现比较简单:
1 | export type TupleToUnion<T extends any[]> = T[number]; |
技巧提示:
T[number]
这种语法可以比较方便地实现 Tuple to Union
Deep Readonly
使用:
1 | type X = { |
实现:
1 | export type DeepReadonly<T> = { |
技巧提示:
- 通过
keyof T[P] extends never
这种方式判断是否有子属性,可以完成深度遍历
Merge 以及其他 interface 相关
使用:
1 | type foo = { |
Merge 的功能和 typescript 提供的很多内置功能函数很像,这个题目实现的方式很多,我这里给出一个比较简洁的方式:
1 | export type Merge<T, U> = { |
类似 Merge,我们在再给出一些其他的简单的类似 type 相关的操作
1 | // Diff:选出两个类型中不同属性: |
一些技巧提示:
- 通过组合 Typescript 内置的功能函数,我们可以完成很多复杂的业务需求。
- 通过
-readonly
来减去修饰符。 - 通过
[P in keyof T]-?: T[P];
这种方式来减去可选修饰符(Typescript 内置的 Required 就是这么实现的)。 - 通过
K in keyof T as T[K]
获取一个属性的类型,结合 extends 做条件判断。
TrimLeft 以及字符串相关
使用:
1 | type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World ' |
实际上字符串操作在 Typescript 中应该用的并不是很多,而这也是 Typescript 比较后面(4.0+)才逐渐完善的功能。
这个题目的解答:
1 | type Space = " " | "\n" | "\t"; |
实际上,我们如果知道可以这样写,还可以实现很多的类似的方式,比如可以很方便地实现 Trim
和 TrimRight
(由于相似度非常高,这两个不再罗列答案),以及 Replace
、ReplaceAll
、DropChar
等,甚至还可以比较方便地实现类型转换,如 ParseInt
。
1 | // Replace |
另外,基于操作字符串的能力,我们还可以实现更多,比如:
- StringToUnion 字符串转联合类型,
123
->"1" | "2" | "3"
- KebabCase 字符串格式转换,
FooBarBaz -> foo-bar-baz
等等,这些笔者没有列举具体实现,是因为认为大部分开发中用的还是不多的,如果你直接一眼就能想出方案,可能也不用去看了。
Append Argument
使用:
1 | type Fn = (a: number, b: string) => number |
这个例子可能会在我们日常开发中用到,而且可以让我们回顾如何操作函数类型:
1 | export type AppendArgument<Fn extends (...args: any) => any, A> = Fn extends ( |
Append to object
使用:
1 | type Test = { id: '1' } |
注意和 Merge
有所区别,这里是针对 Object
来操作
1 | export type AppendToObject<T, U, V> = T extends Record<string, any> |
其他
Typescript 特性相对完备,基于此可以完成非常复杂的需求,甚至使用 Typescript 来编写一个 Typescript Checker,不过笔者认为,对于时间精力有限的一般工作中的开发者来说,知道“可以这样做”,并且在适当的时候可以通过简单的资料查阅完成需求,这一点可能更重要。
经过权衡,本文中只列举了部分 type–challenges 中的内容,如果你还想了解更多,不妨看看原 github 仓库。