近期用了一定时间通读了 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 的一部分简单和中等的题目就是实现这些内置工具函数的二次实现,这部分内容我在本文基本没有重复罗列,但是建议大家去阅读官方实现,一般来说也都比较短小,容易理解
数组第一个元素原文地址
使用:
1 2 type arr1 = ['a' , 'b' , 'c' ]type head1 = First<arr1>
实现:
1 2 3 export type First<T extends any []> = T extends [infer first, ...any[]] ? first : never ;
除了本题目,还有许多其他类似的实现::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 export type Last<T extends any []> = T extends [...any, infer L] ? L : never ;export type Pop<T extends any > = T extends [...infer P, infer R] ? P : never ;export type Push<T extends any [], U> = [...T, U];export type UnShift<T extends any [], U> = [U, ...T];export type Shift<T> = T extends [infer F,... infer R] ? R : undefined export type Concat <T extends any [], V extends any []> = [...T, ...V];export type Reverse<T> = T extends [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : Ttype Filtered = FilterOut<[1 , 2 , null , 3 ], null > export type FilterOut<T extends any [], F> = T extends [infer First, ...infer Rest] ? First extends F ? FilterOut<Rest, F> : [First, ...FilterOut<Rest, F>] : []
技巧提示:
通过 extends 加三目运算符,完成条件判断。
通过类似 infer first, ...any[] 或者 [infer First, ...infer Rest] 这种方式来展开数组类型。
另外我们还可以使用 [...T, U] 来扩充数组的类型定义。
Tuple to Union原文地址
使用:
1 2 type Arr = ['1' , '2' , '3' ]type Test = TupleToUnion<Arr>
实现比较简单:
1 export type TupleToUnion<T extends any []> = T[number ];
技巧提示:
T[number] 这种语法可以比较方便地实现 Tuple to Union
Deep Readonly原文地址
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type X = { x : { a : 1 b : 'hi' } y : 'hey' } type Expected = { readonly x: { readonly a: 1 readonly b: 'hi' } readonly y: 'hey' } type Todo = DeepReadonly<X>
实现:
1 2 3 export type DeepReadonly<T> = { readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>; };
技巧提示:
通过 keyof T[P] extends never 这种方式判断是否有子属性,可以完成深度遍历
Merge 以及其他 interface 相关原文地址
使用:
1 2 3 4 5 6 7 8 9 10 type foo = { name : string ; age: string ; } type coo = { age : number ; sex: string } type Result = Merge<foo,coo>;
Merge 的功能和 typescript 提供的很多内置功能函数很像,这个题目实现的方式很多,我这里给出一个比较简洁的方式:
1 2 3 4 5 export type Merge<T, U> = { [P in Exclude<keyof T, keyof U>]: T[P]; } & { [G in keyof U]: U[G]; };
类似 Merge,我们在再给出一些其他的简单的类似 type 相关的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 export type Diff<T, U> = Omit<T & U, keyof T & keyof U>;export type PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never ]: T[K]; }; export type RequiredByKeys<T, K = keyof T> = SimpleMerge< Partial<Pick<T, Exclude<keyof T, K>>>, Required<Pick<T, Extract<keyof T, K>>> >; export type Mutable<T> = { -readonly [K in keyof T]: T[K] } export type GetRequired<T> = { [P in keyof T as T[P] extends Required<T>[P] ? P : never ]: T[P] } export type RequiredKeys<T> = keyof GetRequired<T>export type GetOptional<T> = { [P in keyof T as T[P] extends Required<T>[P] ? never : P]: T[P] } export type OptionalKeys<T> = keyof GetOptional<T>
一些技巧提示:
通过组合 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 ' >
实际上字符串操作在 Typescript 中应该用的并不是很多,而这也是 Typescript 比较后面(4.0+)才逐渐完善的功能。 这个题目的解答:
1 2 3 4 type Space = " " | "\n" | "\t" ;export type TrimLeft<S extends string > = S extends `${Space} ${infer Rest} ` ? TrimLeft<Rest> : S;
实际上,我们如果知道可以这样写,还可以实现很多的类似的方式,比如可以很方便地实现 Trim 和 TrimRight (由于相似度非常高,这两个不再罗列答案),以及 Replace、ReplaceAll、DropChar 等,甚至还可以比较方便地实现类型转换,如 ParseInt。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export type Replace< S extends string , From extends string , To extends string > = From extends "" ? S : S extends `${infer Left} ${From} ${infer Right} ` ? `${Left} ${To} ${Right} ` : S; export type ReplaceAll< S extends string , From extends string , To extends string > = From extends "" ? S : S extends `${infer Left} ${From} ${infer Right} ` ? `${Left} ${To} ${ReplaceAll<`${Right} ` , From, To>} ` : S; export type DropChar<S, C> = S extends `${infer H} ${infer R} ` ? H extends C ? `${DropChar<R, C>} ` : `${H} ${DropChar<R, C>} ` : "" ; export type ParseInt<T extends string > = T extends `${infer Digit extends number } ` ? Digit : never
另外,基于操作字符串的能力,我们还可以实现更多,比如:
等等,这些笔者没有列举具体实现,是因为认为大部分开发中用的还是不多的,如果你直接一眼就能想出方案,可能也不用去看了。
Append Argument原文地址
使用:
1 2 3 4 type Fn = (a: number , b: string ) => number type Result = AppendArgument<Fn, boolean >
这个例子可能会在我们日常开发中用到,而且可以让我们回顾如何操作函数类型:
1 2 3 4 5 export type AppendArgument<Fn extends (...args: any ) => any , A> = Fn extends ( ...args: infer Args ) => infer Res ? (...arg: [...Args, A] ) => Res : never ;
Append to object原文地址
使用:
1 2 type Test = { id : '1' }type Result = AppendToObject<Test, 'value' , 4 >
注意和 Merge 有所区别,这里是针对 Object 来操作
1 2 3 4 5 export type AppendToObject<T, U, V> = T extends Record<string , any > ? U extends string ? { [K in keyof T | U]: K extends U ? V : T[K] } : T : T;
其他Typescript 特性相对完备,基于此可以完成非常复杂的需求,甚至使用 Typescript 来编写一个 Typescript Checker ,不过笔者认为,对于时间精力有限的一般工作中的开发者来说,知道“可以这样做”,并且在适当的时候可以通过简单的资料查阅完成需求,这一点可能更重要。
经过权衡,本文中只列举了部分 type–challenges 中的内容,如果你还想了解更多,不妨看看原 github 仓库。