解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 49~56 题。
实现 Flip<T>
,将对象 T
中 Key 与 Value 对调:
Flip<{ a: "x", b: "y", c: "z" }>; // {x: 'a', y: 'b', z: 'c'} Flip<{ a: 1, b: 2, c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'} Flip<{ a: false, b: true }>; // {false: 'a', true: 'b'}
在 keyof
描述对象时可以通过 as
追加变形,所以这道题应该这样处理:
type Flip<T> = { [K in keyof T as T[K]]: K }
由于 Key 位置只能是 String or Number,所以 T[K]
描述 Key 会显示错误,我们需要限定 Value 的类型:
type Flip<T extends Record<string, string | number>> = { [K in keyof T as T[K]]: K }
但这个答案无法通过测试用例 Flip<{ pi: 3.14; bool: true }>
,原因是 true
不能作为 Key。只能用字符串 'true'
作为 Key,所以我们得强行把 Key 位置转化为字符串:
// 本题答案 type Flip<T extends Record<string, string | number | boolean>> = { [K in keyof T as `${T[K]}`]: K }
用 TS 实现斐波那契数列计算:
type Result1 = Fibonacci<3> // 2 type Result2 = Fibonacci<8> // 21
由于测试用例没有特别大的 Case,我们可以放心用递归实现。JS 版的斐波那契非常自然,但 TS 版我们只能用数组长度模拟计算,代码写起来自然会比较扭曲。
首先需要一个额外变量标记递归了多少次,递归到第 N 次结束:
type Fibonacci<T extends number, N = [1]> = N['length'] extends T ? ( // xxx ) : Fibonacci<T, [...N, 1]>
上面代码每次执行都判断是否递归完成,否则继续递归并把计数器加一。我们还需要一个数组存储答案,一个数组存储上一个数:
// 本题答案 type Fibonacci< T extends number, N extends number[] = [1], Prev extends number[] = [1], Cur extends number[] = [1] > = N['length'] extends T ? Prev['length'] : Fibonacci<T, [...N, 1], Cur, [...Prev, ...Cur]>
递归时拿 Cur
代替下次的 Prev
,用 [...Prev, ...Cur]
代替下次的 Cur
,也就是说,下次的 Cur
符合斐波那契定义。
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 41~48 题。
实现 TS 版本的 Object.entries
:
interface Model { name: string; age: number; locations: string[] | null; } type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];
经过前面的铺垫,大家应该熟悉了 TS 思维思考问题,这道题看到后第一个念头应该是:如何先把对象转换为联合类型?这个问题不解决,就无从下手。
对象或数组转联合类型的思路都是类似的,一个数组转联合类型用 [number]
作为下标:
['1', '2', '3']['number'] // '1' | '2' | '3'
对象的方式则是 [keyof T]
作为下标:
type ObjectToUnion<T> = T[keyof T]
再观察这道题,联合类型每一项都是数组,分别是 Key 与 Value,这样就比较好写了,我们只要构造一个 Value 是符合结构的对象即可:
type ObjectEntries<T> = { [K in keyof T]: [K, T[K]] }[keyof T]
为了通过单测 ObjectEntries<{ key?: undefined }>
,让 Key 位置不出现 undefined
,需要强制把对象描述为非可选 Key:
type ObjectEntries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]
为了通过单测 ObjectEntries<Partial<Model>>
,得将 Value 中 undefined
移除:
// 本题答案 type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined> type ObjectEntries<T> = { [K in keyof T]-?: [K, RemoveUndefined<T[K]>] }[keyof T]
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 33~40 题。
用 TS 实现 MinusOne
将一个数字减一:
type Zero = MinusOne<1> // 0 type FiftyFour = MinusOne<55> // 54
TS 没有 “普通” 的运算能力,但涉及数字却有一条生路,即 TS 可通过 ['length']
访问数组长度,几乎所有数字计算都是通过它推导出来的。
这道题,我们只要构造一个长度为泛型长度 -1 的数组,获取其 ['length']
属性即可,但该方案有一个硬伤,无法计算负值,因为数组长度不可能小于 0:
// 本题答案 type MinusOne<T extends number, arr extends any[] = []> = [ ...arr, '' ]['length'] extends T ? arr['length'] : MinusOne<T, [...arr, '']>
该方案的原理不是原数字 -1,而是从 0 开始不断加 1,一直加到目标数字减一。但该方案没有通过 MinusOne<1101>
测试,因为递归 1000 次就是上限了。
还有一种能打破递归的思路,即:
type Count = ['1', '1', '1'] extends [...infer T, '1'] ? T['length'] : 0 // 2
也就是把减一转化为 extends [...infer T, '1']
,这样数组 T
的长度刚好等于答案。那么难点就变成了如何根据传入的数字构造一个等长的数组?即问题变成了如何实现 CountTo<N>
生成一个长度为 N
,每项均为 1
的数组,而且生成数组的递归效率也要高,否则还会遇到递归上限的问题。
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 25~32 题。
实现 Diff<A, B>
,返回一个新对象,类型为两个对象类型的 Diff:
type Foo = { name: string age: string } type Bar = { name: string age: string gender: number } Equal<Diff<Foo, Bar> // { gender: number }
首先要思考 Diff 的计算方式,A 与 B 的 Diff 是找到 A 存在 B 不存在,与 B 存在 A 不存在的值,那么正好可以利用 Exclude<X, Y>
函数,它可以得到存在于 X
不存在于 Y
的值,我们只要用 keyof A
、keyof B
代替 X
与 Y
,并交替 A、B 位置就能得到 Diff:
// 本题答案 type Diff<A, B> = { [K in Exclude<keyof A, keyof B> | Exclude<keyof B, keyof A>]: K extends keyof A ? A[K] : ( K extends keyof B ? B[K]: never ) }
Value 部分的小技巧我们之前也提到过,即需要用两套三元运算符保证访问的下标在对象中存在,即 extends keyof
的语法技巧。
解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 17~24 题。
实现 Permutation
类型,将联合类型替换为可能的全排列:
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
看到这题立马联想到 TS 对多个联合类型泛型处理是采用分配律的,在第一次做到 Exclude
题目时遇到过:
Exclude<'a' | 'b', 'a' | 'c'> // 等价于 Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>
所以这题如果能 “递归触发联合类型分配率”,就有戏解决啊。但触发的条件必须存在两个泛型,而题目传入的只有一个,我们只好创造第二个泛型,使其默认值等于第一个:
type Permutation<T, U = T>
这样对本题来说,会做如下展开:
Permutation<'A' | 'B' | 'C'> // 等价于 Permutation<'A' | 'B' | 'C', 'A' | 'B' | 'C'> // 等价于 Permutation<'A', 'A' | 'B' | 'C'> | Permutation<'B', 'A' | 'B' | 'C'> | Permutation<'C', 'A' | 'B' | 'C'>
对于 Permutation<'A', 'A' | 'B' | 'C'>
来说,排除掉对自身的组合,可形成 'A', 'B'
,'A', 'C'
组合,之后只要再递归一次,再拼一次,把已有的排除掉,就形成了 A
的全排列,以此类推,形成所有字母的全排列。