精讀《Promise.all, Replace, Type Lookup...》

黃子毅發表於2022-06-20

解決 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> = 
  S extends `${infer A}${From}${infer B}` ? (
    From extends '' ? `${A}${To}${B}` : ReplaceAll<`${A}${To}${B}`, From, To>
  ) : S

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 和遞迴的熟練使用。

討論地址是:精讀《Promise.all, Replace, Type Lookup...》· Issue #425 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章