從 React 源碼的類型定義中,我學到了什么?
今天看了下 React 的類型定義,也就是 @types/react 包下的 index.d.ts,發(fā)現(xiàn)了一些有趣的寫法。
這篇文章就分享下這些寫法,估計大部分人都不知道:
提取可選索引的值
首先,我看到了這樣一段類型邏輯:
這段邏輯就是取索引類型的 ref 索引的值,但是是通過模式匹配的方式,把提取的類型放到 infer 聲明的局部變量 R 里返回的。
簡化一下就是這樣的:
提取 Props 的 ref 索引的值的類型返回。
我在想,這么麻煩干什么,直接 Props['ref'] 不就能拿到 ref 索引的值么?
于是我就改成了這樣:
然后試了下:
不對呀,人家這是可選索引,值的類型是包含 undefined 的聯(lián)合類型。
那就 Exclude 下不就行了:
這樣也比那個 infer 的方式簡潔呀,為啥 React 類型定義都是用的 infer 取的可選索引的類型呢?
后來我突然想到,如果這個 ref 值的類型就是 undefined 呢?
我試了下:
確實,我那樣寫是有問題的,如果值的類型本來就是 undefined,Exclude 掉 undefined 后就是 never 了,而人家那種方式就沒問題:
于是我就加一下 undefined 的處理:
這樣就行了。
對比了下兩種寫法:
確實還是 React 的那種寫法更簡潔。
對了,那上面那層判斷呢?
這個判斷沒必要的吧,如果沒有 ref,那 Props['ref'] 不就是返回 never 么,沒必要單獨判斷呀?
然后我就看到了這樣一段注釋:
在 ts 3.0 中,如果索引類型沒有對應的索引,那返回的類型是 {} 而不是 never。
原來如此,這個 'ref' extends keyof Props 是為了做兼容的呀,不是沒意義。
這就是我從這個類型中學到的兩個知識點:
- 索引訪問 Obj[Key] 和 infer 提取和都可以取到索引類型的某個索引的值,但是當處理可選索引的時候,用 infer 更簡潔一些,因為前者要取出類型之后再單獨處理下 undefined,而后者在 infer 的時候就順便處理了 undefined。
- ts 3.0 中如果索引類型沒有對應的索引,返回的是 {} 不是 never,如果對兼容性要求高的話,可以用 'xx' in keyOf Obj 的方式做下兼容。
我們從這個類型里學到了不少東西,再來看下第二個類型:
索引類型和 any、never 的處理
然后我又看到了這樣一個類型,
先試一下它的功能,傳入兩個索引類型:
看下結果:
這是些啥啊,誰能看得懂呀。
其實這只是因為 TS 沒有計算出最終的類型而已,用到的時候才會計算,所以我們可以這樣處理下:
Copy 的高級類型是通過映射類型的語法構造了一個新的索引類型,它的功能是原封不動的復制一個索引類型。
類型參數(shù) Obj 約束為索引類型,也就是 Record。key 保持不變,也就是 Key in keyof Obj,value 也保持不變,也就是 Obj[Key]。
因為重新生成的類型的過程中要做計算,所以那個類型就能提示出最終的結果了:
所以說,這個類型的作用是兩個索引類型 A,B,只有 A 中有的就保留,A、B 都有的變?yōu)榭蛇x,B 有但 A 沒有的變?yōu)榭蛇x。
那這段邏輯具體是怎么用 TS 實現(xiàn)的呢?
我們先來過一下 TS 這些內置的高級類型:
Pick
Pick 的作用是通過映射類型的語法構造一個新的索引類型,根據傳入的 Key 對索引做下過濾:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
測試下:
Partial
Partial 也是通過映射類型的語法構造一個新的索引類型,但是會把索引變?yōu)榭蛇x:
type Partial<T> = { [P in keyof T]?: T[P]; };
測試下:
Extract
Extract 是取兩個聯(lián)合類型都包含的部分,也就是取交集:
type Extract = T extends U ? T : never;
測試下:
Exclude
Exclude 是從聯(lián)合類型 A 中去掉聯(lián)合類型 B 中的類型,也就是取差集:
type Extract = T extends U ? T : never;
測試下:
學會了用 Pick、Partial、Exclude、Extract 這些高級類型,那上面的那段邏輯我們就知道怎么實現(xiàn)了:
只有 A 中有的就保留的邏輯是:Pick>。
A、B 都有的變?yōu)榭蛇x:Partial>>。
B 中有但 A 中沒有的也變?yōu)榭蛇x:Partial>>。
這樣,這個類型的主要邏輯我們就理清了:
把三部分計算結果的索引類型取交叉類型,就會合并到一起。
那前面那兩個判斷是啥?
P extends any 還有這個 string extends keyof P,這倆都是做啥的?
P extends any 這個是因為聯(lián)合類型當作為類型參數(shù)出現(xiàn)在條件類型左邊時,會把每個類型單獨傳入做計算,最后把計算結果合并成聯(lián)合類型,這個特性叫做分布式條件類型。
比如這樣一個聯(lián)合類型:
type Union = 'a' | 'b' | 'c';
我們想把其中的 a 大寫,就可以這樣寫:
type UppercaseA<Item extends string> =
Item extends 'a' ? Uppercase<Item> : Item;
因為 Item 是類型參數(shù),出現(xiàn)在了條件類型的左邊,而且傳入的是聯(lián)合類型,那就觸發(fā)分發(fā)特性,會把每個類型單獨傳入做計算,然后把結果合并成聯(lián)合類型。
所以這里的 P extends any 的作用就是觸發(fā)聯(lián)合類型特性的,從而讓這個類型能正確處理聯(lián)合類型。不然聯(lián)合類型整個傳入的話,后面怎么做計算。
這里的 P extends any 換成 P extends P 也可以,都是一樣的作用。
那后面那段代碼 string extends keyof P 是啥意思?
這個我確實想了一段時間,如果 { a: 1, b: 2} 這樣的索引類型,keyof 的結果是 'a' | 'b',而如果是數(shù)組類型,那 keyof 的結果是 0 | 1 | 'length' | 'toString' | ...
什么類型的 keyof 結果是 string 呢?
突然,我想起了前幾天學到的一個知識點:用 keyof any 代替 string | number | symbol 更靈活:
而且我試了下 never 的 keyof 結果也是這個:
所以說 string extends keyof P 就可以排除 any 和 never 的情況!
妙呀,還能這么區(qū)分 any 和 never。
所以說,這個類型的邏輯我們已經理清了:
這個類型的功能是保留只有 A 有的索引,把 A、B 都有的索引變?yōu)榭蛇x,把只有 B 有的索引變?yōu)榭蛇x。
而且處理了聯(lián)合類型的情況。
如果傳入的是 any 或者 never,不做處理,直接返回。
這個類型里我們也學到了不少東西。
總結
我看了下 @types/react 的類型定義,學到了不少東西:
- 可選索引的值的提取,用 infer 比 Obj[key] 更方便,因為前者只需要 Obj[Key] extends { xxx?: infer Value: undefined},而后者需要先排除值的類型就是 undefined 的情況,然后再用 Exclude 去掉類型中的 undefined。
- ts 3.0 中取索引類型沒有的索引會返回 {} 而不是 never,需要兼容的話可以單獨做下判斷:'xxx' in keyof Obj。
- 處理索引類型可以綜合用 Pick、Partial、Exclude、Extract 等內置高級類型對每一部分索引做處理,然后取交叉類型來合并到一起。
- P extends any 和 P extends P 的作用是觸發(fā)聯(lián)合類型的分發(fā)特性的,加上這段處理才能正確處理聯(lián)合類型,會把每個類型單獨傳入做計算,最后把結果合并成聯(lián)合類型。
- string extends keyof Obj 可以判斷出 any 和 never 類型,只有這兩種類型取 keyof 的結果是 string | nubmer | symbole,包含 string。
而且,還講了一個小技巧:
ts 類型只有計算的時候才會求值,如果是索引類型,可以用映射類型的語法創(chuàng)建個一摸一樣的索引類型,因為用到了,就會做計算,從而就可以顯示出最終的類型。
不得不說,React 類型定義做的挺完善的,考慮到了各種類型的處理,也考慮到了低版本的兼容,從中還是能學到不少東西的。