2022-09-26TypeScript00
请注意,本文编写于 549 天前,最后修改于 549 天前,其中某些信息可能已经过时。

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

精读

Promise.all

实现函数 PromiseAll,输入 PromiseLike,输出 Promise<T>,其中 T 是输入的解析结果:

const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])

该题难点不在 Promise 如何处理,而是在于 { [K in keyof T]: T[K] } 在 TS 同样适用于描述数组,这是 JS 选手无论如何也想不到的:

// 本题答案
declare function PromiseAll<T>(values: T): Promise<{
  [K in keyof T]: T[K] extends Promise<infer U> ? U : T[K]
}>

不知道是 bug 还是 feature,TS 的 { [K in keyof T]: T[K] } 能同时兼容元组、数组与对象类型。

Type Lookup

实现 LookUp<T, P>,从联合类型 T 中查找 typeP 的项并返回:

interface Cat {
  type: 'cat'
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}

interface Dog {
  type: 'dog'
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
  color: 'brown' | 'white' | 'black'
}

type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`

该题比较简单,只要学会灵活使用 inferextends 即可:

// 本题答案
type LookUp<T, P> = T extends {
  type: infer U
} ? (
  U extends P ? T : never
) : never

联合类型的判断是一个个来的,所以我们只要针对每一个单独写判断就行了。上面的解法中,我们先利用 extend + infer 锁定 T 的类型是包含 type key 的对象,且将 infer U 指向了 type,所以在内部再利用三元运算符判断 U extends P ? 就能将 type 命中的类型挑出来。

笔者翻了下答案,发现还有一种更高级的解法:

// 本题答案
type LookUp<U extends { type: any }, T extends U['type']> = U extends { type: T } ? U : never

该解法更简洁,更完备:

  • 在泛型处利用 extends { type: any }extends U['type'] 直接锁定入参类型,让错误校验更早发生。
  • T extends U['type'] 精确缩小了参数 T 范围,可以学到的是,之前定义的泛型 U 可以直接被后面的新泛型使用。
  • U extends { type: T } 是一种新的思考角度。在第一个答案中,我们的思维方式是 “找到对象中 type 值进行判断”,而第二个答案直接用整个对象结构 { type: T } 判断,是更纯粹的 TS 思维。

Trim Left

实现 TrimLeft<T>,将字符串左侧空格清空:

type trimed = TrimLeft<'  Hello World  '> // expected to be 'Hello World  '

在 TS 处理这类问题只能用递归,不能用正则。比较容易想到的是下面的写法:

// 本题答案
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T

即如果字符串前面包含空格,就把空格去了继续递归,否则返回字符串本身。掌握该题的关键是 infer 也能用在字符串内进行推导。

Trim

实现 Trim<T>,将字符串左右两侧空格清空:

type trimmed = Trim<'  Hello World  '> // expected to be 'Hello World'

这个问题简单的解法是,左右都 Trim 一下:

// 本题答案
type Trim<T extends string> = TrimLeft<TrimRight<T>>
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T
type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T

这个成本很低,性能也不差,因为单写 TrimLeftTrimRight 都很简单。

如果不采用先 Left 后 Right 的做法,想要一次性完成,就要有一些 TS 思维了。比较笨的思路是 “如果左边有空格就切分左边,或者右边有空格就切分右边”,最后写出来一个复杂的三元表达式。比较优秀的思路是利用 TS 联合类型:

// 本题答案
type Trim<T extends string> =  T extends ` ${infer R}` | `${infer R} ` ? Trim<R> : T

extends 后面还可以跟联合类型,这样任意一个匹配都会走到 Trim<R> 递归里。这就是比较难说清楚的 TS 思维,如果没有它,你只能想到三元表达式,但一旦理解了联合类型还可以在 extends 里这么用,TS 帮你做了 N 元表达式的能力,那么写出来的代码就会非常清秀。

Capitalize

实现 Capitalize<T> 将字符串第一个字母大写:

type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'

如果这是一道 JS 题那就简单到爆,可题目是 TS 的,我们需要再度切换为 TS 思维。

首先要知道利用基础函数 Uppercase 将单个字母转化为大写,然后配合 infer 就不用多说了:

type MyCapitalize<T extends string> = T extends `${infer F}${infer U}` ? `${Uppercase<F>}${U}` : T

Replace

实现 TS 版函数 Replace<S, From, To>,将字符串 From 替换为 To

type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'

From 夹在字符串中间,前后用两个 infer 推导,最后输出时前后不变,把 From 换成 To 就行了:

// 本题答案
type Replace<S extends string, From extends string, To extends string,> = 
  S extends `${infer A}${From}${infer B}` ? `${A}${To}${B}` : S

ReplaceAll

实现 ReplaceAll<S, From, To>,将字符串 From 替换为 To

type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'

该题与上题不同之处在于替换全部,解法肯定是递归,关键是何时递归的判断条件是什么。经过一番思考,如果 infer From 能匹配到不就说明还可以递归吗?所以加一层三元判断 From extends '' 即可:

// 本题答案
type ReplaceAll<S extends string, From extends string, To extends string> = 
  From extends '' ? S : (
    S extends `${infer A}${From}${infer B}` ? (
      From extends '' ? `${A}${To}${B}` : `${A}${To}${ReplaceAll<B, From, To>}`
    ) : S
  )

补充一些细节:

  1. 如果替换文本为空字符串需要跳过,否则会匹配第二个任意字符。
  2. 为了防止替换完后结果可以再度匹配,对递归形式做一下调整,下次递归直接从剩余部分开始。

Append Argument

实现类型 AppendArgument<F, E>,将函数参数拓展一个:

type Fn = (a: number, b: string) => number

type Result = AppendArgument<Fn, boolean> 
// expected be (a: number, b: string, x: boolean) => number

该题很简单,用 infer 就行了:

// 本题答案
type AppendArgument<F, E> = F extends (...args: infer T) => infer R ? (...args: [...T, E]) => R : F

总结

这几道题都比较简单,主要考察对 infer 和递归的熟练使用。

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!