解決 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
中查詢 type
為 P
的項並返回:
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`
該題比較簡單,只要學會靈活使用 infer
與 extends
即可:
// 本題答案
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
這個成本很低,效能也不差,因為單寫 TrimLeft
與 TrimRight
都很簡單。
如果不採用先 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 許可證)