解決 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 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
的語法技巧。
AnyOf
實現 AnyOf
函式,任意項為真則返回 true
,否則返回 false
,空陣列返回 false
:
type Sample1 = AnyOf<[1, '', false, [], {}]> // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]> // expected to be false.
本題有幾個問題要思考:
第一是用何種判定思路?像這種判斷陣列內任意元素是否滿足某個條件的題目,都可以用遞迴的方式解決,具體是先判斷陣列第一項,如果滿足則繼續遞迴判斷剩餘項,否則終止判斷。這樣能做但比較麻煩,還有種取巧的辦法是利用 extends Array<>
的方式,讓 TS 自動幫你遍歷。
第二個是如何判斷任意項為真?為真的情況很多,我們嘗試列舉為假的 Case:0
undefined
''
undefined
null
never
[]
。
結合上面兩個思考,本題作如下解答不難想到:
type Falsy = '' | never | undefined | null | 0 | false | []
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true
但會遇到這個測試用例沒通過:
AnyOf<[0, '', false, [], {}]>
如果此時把 {}
補在 Falsy
裡,會發現除了這個 case 外,其他判斷都掛了,原因是 { a: 1 } extends {}
結果為真,因為 {}
並不表示空物件,而是表示所有物件型別,所以我們要把它換成 Record<PropertyKey, never>
,以鎖定空物件:
// 本題答案
type Falsy = '' | never | undefined | null | 0 | false | [] | Record<PropertyKey, never>
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true
IsNever
實現 IsNever
判斷值型別是否為 never
:
type A = IsNever<never> // expected to be true
type B = IsNever<undefined> // expected to be false
type C = IsNever<null> // expected to be false
type D = IsNever<[]> // expected to be false
type E = IsNever<number> // expected to be false
首先我們可以毫不猶豫的寫下一個錯誤答案:
type IsNever<T> = T extends never ? true :false
這個錯誤答案離正確答案肯定是比較近的,但錯在無法判斷 never
上。在 Permutation
全排列題中我們就認識到了 never
在泛型中的特殊性,它不會觸發 extends
判斷,而是直接終結,致使判斷無效。
而解法也很簡單,只要繞過 never
這個特性即可,包一個陣列:
// 本題答案
type IsNever<T> = [T] extends [never] ? true :false
IsUnion
實現 IsUnion
判斷是否為聯合型別:
type case1 = IsUnion<string> // false
type case2 = IsUnion<string|number> // true
type case3 = IsUnion<[string|number]> // false
這道題完全是腦筋急轉彎了,因為 TS 肯定知道傳入型別是否為聯合型別,並且會對聯合型別進行特殊處理,但並沒有暴露聯合型別的判斷語法,所以我們只能對傳入型別進行測試,推斷是否為聯合型別。
我們到現在能想到聯合型別的特徵只有兩個:
- 在 TS 處理泛型為聯合型別時進行分發處理,即將聯合型別拆解為獨立項一一進行判定,最後再用
|
連線。 - 用
[]
包裹聯合型別可以規避分發的特性。
所以怎麼判定傳入泛型是聯合型別呢?如果泛型進行了分發,就可以反推出它是聯合型別。
難點就轉移到了:如何判斷泛型被分發了?首先分析一下,分發的效果是什麼樣:
A extends A
// 如果 A 是 1 | 2,分發結果是:
(1 extends 1 | 2) | (2 extends 1 | 2)
也就是這個表示式會被執行兩次,第一個 A
在兩次值分別為 1
與 2
,而第二個 A
在兩次執行中每次都是 1 | 2
,但這兩個表示式都是 true
,無法體現分發的特殊性。
此時要利用包裹 []
不分發的特性,即在分發後,由於在每次執行過程中,第一個 A
都是聯合型別的某一項,因此用 []
包裹後必然與原始值不相等,所以我們在 extends
分發過程中,再用 []
包裹 extends
一次,如果此時匹配不上,說明產生了分發:
type IsUnion<A> = A extends A ? (
[A] extends [A] ? false : true
) : false
但這段程式碼依然不正確,因為在第一個三元表示式括號內,A
已經被分發,所以 [A] extends [A]
即便對聯合型別也是判定為真的,此時需要用原始值代替 extends
後面的 [A]
,騷操作出現了:
type IsUnion<A, B = A> = A extends A ? (
[B] extends [A] ? false : true
) : false
雖然我們申明瞭 B = A
,但過程中因為 A
被分發了,所以執行時 B
是不等於 A
的,才使得我們達成目的。[B]
放 extends
前面是因為,B
是未被分發的,不可能被分發後的結果包含,所以分發時此條件必定為假。
最後因為測試用例有一個 never
情況,我們用剛才的 IsNever
函式提前判否即可:
// 本題答案
type IsUnion<A, B = A> = IsNever<A> extends true ? false : (
A extends A ? (
[B] extends [A] ? false : true
) : false
)
從該題我們可以深刻體會到 TS 的怪異之處,即 type X<T> = T extends ...
中 extends
前面的 T
不一定是你看到傳入的 T
,如果是聯合型別的話,會分發為單個型別分別處理。
ReplaceKeys
實現 ReplaceKeys<Obj, Keys, Targets>
將 Obj
中每個物件的 Keys
Key 型別轉化為符合 Targets
物件對應 Key 描述的型別,如果無法匹配到 Targets
則型別置為 never
:
type NodeA = {
type: 'A'
name: string
flag: number
}
type NodeB = {
type: 'B'
id: number
flag: number
}
type NodeC = {
type: 'C'
name: string
flag: number
}
type Nodes = NodeA | NodeB | NodeC
type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.
type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never
本題別看描述很嚇人,其實非常簡單,思路:用 K in keyof Obj
遍歷原始物件所有 Key,如果這個 Key 在描述的 Keys
中,且又在 Targets
中存在,則返回型別 Targets[K]
否則返回 never
,如果不在描述的 Keys
中則用在物件裡本來的型別:
// 本題答案
type ReplaceKeys<Obj, Keys, Targets> = {
[K in keyof Obj] : K extends Keys ? (
K extends keyof Targets ? Targets[K] : never
) : Obj[K]
}
Remove Index Signature
實現 RemoveIndexSignature<T>
把物件 <T>
中 Index 下標移除:
type Foo = {
[key: string]: any;
foo(): void;
}
type A = RemoveIndexSignature<Foo> // expected { foo(): void }
該題思考的重點是如何將物件字串 Key 識別出來,可以用 \`${infer P}\` 是否能識別到 P
來判斷當前是否命中了字串 Key:
// 本題答案
type RemoveIndexSignature<T> = {
[K in keyof T as K extends `${infer P}` ? P : never]: T[K]
}
Percentage Parser
實現 PercentageParser<T>
,解析出百分比字串的符號位與數字:
type PString1 = ''
type PString2 = '+85%'
type PString3 = '-85%'
type PString4 = '85%'
type PString5 = '85'
type R1 = PercentageParser<PString1> // expected ['', '', '']
type R2 = PercentageParser<PString2> // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3> // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4> // expected ["", "85", "%"]
type R5 = PercentageParser<PString5> // expected ["", "85", ""]
這道題充分說明了 TS 沒有正則能力,儘量還是不要做正則的事情 ^_^。
回到正題,如果非要用 TS 實現,我們只能列舉各種場景:
// 本題答案
type PercentageParser<A extends string> =
// +/-xxx%
A extends `${infer X extends '+' | '-'}${infer Y}%`? [X, Y, '%'] : (
// +/-xxx
A extends `${infer X extends '+' | '-'}${infer Y}` ? [X, Y, ''] : (
// xxx%
A extends `${infer X}%` ? ['', X, '%'] : (
// xxx 包括 ['100', '%', ''] 這三種情況
A extends `${infer X}` ? ['', X, '']: never
)
)
)
這道題運用了 infer
可以無限進行分支判斷的知識。
Drop Char
實現 DropChar
從字串中移除指定字元:
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'
這道題和 Replace
很像,只要用遞迴不斷把 C
排除掉即可:
// 本題答案
type DropChar<S, C extends string> = S extends `${infer A}${C}${infer B}` ?
`${A}${DropChar<B, C>}` : S
總結
寫到這,越發覺得 TS 雖然具備圖靈完備性,但在邏輯處理上還是不如 JS 方便,很多設計計算邏輯的題目的解法都不是很優雅。
但是解決這類題目有助於強化對 TS 基礎能力組合的理解與綜合運用,在解決實際型別問題時又是必不可少的。
討論地址是:精讀《Diff, AnyOf, IsUnion...》· Issue #429 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)