2022-09-26TypeScript00

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 49~56 题。

精读

Flip

实现 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
}

Fibonacci Sequence

用 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 符合斐波那契定义。

2022-09-26TypeScript00

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 41~48 题。

精读

ObjectEntries

实现 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]
2022-09-26TypeScript00

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 33~40 题。

精读

MinusOne

用 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 的数组,而且生成数组的递归效率也要高,否则还会遇到递归上限的问题。

2022-09-26TypeScript00

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 25~32 题。

精读

Diff

实现 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 Akeyof B 代替 XY,并交替 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 的语法技巧。

2022-09-26TypeScript00

解决 TS 问题的最好办法就是多练,这次解读 type-challenges Medium 难度 17~24 题。

精读

Permutation

实现 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 的全排列,以此类推,形成所有字母的全排列。