React 中的 TS 類型過濾原來是這麼做的!

大家好,我是零一,相信大家在閱讀同事寫的代碼或者優秀的開源庫的代碼時,一定見過各種各樣的風騷的 TS 寫法,不花點時間下去根本看不懂,換作是我們,可能就直接一個 any 完事了,但是真正當項目體積變大後,你會發現這些 TS 騷操作真的很重要,因爲它能很好地幫助你做靜態類型校驗

今天就來介紹一個在其它開源庫中見到的既花裏胡哨,又實用的 TS 類型——TS 類型過濾

自我介紹

TS 類型過濾,英文名(我自己取的)叫 FilterConditionally,這是它完整的樣子👇

type FilterConditionally<Source, Condition> = Pick<
  Source, 
  {
    [K in keyof Source]: Source[K] extends Condition ? K : never
  }[keyof Source]
>;

別看很複雜,其實非常有用,它可以從一個對象類型中過濾出你想要的,比如:

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}
type NewType = FilterConditionally<Sample, string>
/*
 NewType 最終結果爲:
 {
  a: string;
  b: string
 }
*/

相信大家已經這個類型的作用了,並且你們也很想讀懂它,沒關係,接下來由內而外、一步一步地介紹,一定讓你們完全讀懂,讀不懂評論區來噴我(我說着玩的~)

分步介紹

涉及的知識點比較多,怕有些不熟悉 TS 的同學懵逼,先來介紹其中幾個常見的基礎知識點

開胃小菜

不會耽誤大家多少時間的,會的小夥伴可以直接調過

keyof

關鍵詞 keyof 的名字叫 索引類型查詢操作符,它的作用就像它的字面意思一樣直白:xx 的 key 值

interface Example {
 a: string;
  b: string;
  c: number;
  d: boolean;
}

type Keys = keyof Example   // 等價於 type Keys = 'a' | 'b' | 'c' | 'd'

你可以把 keyof 簡單理解爲 JavaScript 中的 Object.keys

in

關鍵詞 in 可以遍歷枚舉類型,比如:

type Keys = 'a' | 'b' | 'c' | 'd'

type Obj = {
  [T in Keys]: string;  // 遍歷Keys,把每個key都賦值string類型
}
/* 等價於 
  type Obj = {
    a: string;
    b: string;
   c: string;
   d: string;
  }
*/

你可以把 in 簡單理解爲 JavaScript 中 for...inin 的作用

Conditional

第二個知識點是條件判斷,比如:

interface A {}

interface B extends A {}  // B繼承於A

// B是否繼承於A?若是,則爲number類型;若不是,則爲string類型
type C = B extends A ? number : string  // 等價於 type C = number

// A是否繼承於B?若是,則爲number類型;若不是,則爲string類型
type D = A extends B ? number : string  // 等價於 type D = string

你可以把 A extends B ? number : string 簡單理解爲 JavaScript 中的三元運算符

泛型

泛型我就不多做介紹了,不太瞭解的小夥伴可以直接看 TS 文檔——泛型 [1]

正餐開始

剛剛介紹完 "開胃小菜",那就趁熱打鐵看一個簡單的類型

type MarkUnwantedTypesAsNever<Source, Condition> ={
  [K in keyof Source]: Source[K] extends Condition ? K : never
}

一句話介紹這個類型的作用就是:遍歷一個對象類型,將不想要的類型標記爲 never

舉個例子🌰

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}

// 我只想要Example類型中的string類型的key,非string的就標記爲never
type MyType = MarkUnwantedTypesAsNever<Example, string>
/*
 等價於:
 type MyType = {
  a: 'a';
  b: 'b';
  c: never;
  d: never;
 }
*/

稍微講一下小細節,[K in keyof Example] 遍歷了 Example 這個對象類型,然後用條件判斷 Example[K] extends string ? K : never 給對應的 key 值賦值,假設遍歷第一個 key 值爲 a,那麼 Example[K] = Example[a] = string,此時就是 string extends string ? 'a' : neverstring 肯定是繼承於 string 的,所以纔會有這樣一個結果

此時大家心頭一驚,爲什麼要把類型搞成這樣??我們最後想要的結果不是要拿到一個 { a:string; b:string } 的類型嗎?別急,後面還有別的操作

再來看一個索引訪問接口屬性的小知識點

type Value = {name: "zero2one"}["name"]  // 等價於 type Value = "zero2one"

你可以把它簡單理解成 JavaScript 中訪問對象某個 key 對應的 value

而在 TS 中還有另一種情況:

type Value = {
  name: "zero2one"; 
  age: 23
}["name" | "age"]

// 等價於 type Value = "zero2one" | 23

而值爲 neverkey 值是無法被訪問到的:

type Value = {
  name: "zero2one"; 
  age: never
}["name" | "age"]

// 等價於 type Value = "zero2one"

所以接下來可以看更復雜的類型了

type MarkUnwantedTypesAsNever<Source, Condition> ={
  [K in keyof Source]: Source[K] extends Condition ? K : never
}[keyof Source]

我們巧妙地利用 keyof 關鍵詞去遍歷訪問所有的接口屬性

// 借用一下剛纔例子的結果
type MyType = {
   a: 'a';
  b: 'b';
  c: never;
  d: never;
}['a' | 'b' | 'c' | 'd']

/*
 等價於:
 type MyType = 'a' | 'b'
*/

到此爲止,我們所做的事情就是:把目標對象類型中想要類型的 key 值篩選了出來

別急別急,離成功就差一步之遙

最後登場的就是 Pick ,這個類型是 TS 內置的,簡單瞭解一下它的作用

// Pick類型的實現
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

你可以不去詳細地讀懂它的實現,只需要知道 Pick 的作用就是:篩選出類型T 中指定的某些屬性

舉個簡單的例子:

interface A {
  a: 1;
  b: 2;
  c: 3;
  d: 4;
}

type C = Pick<A, 'a' | 'c'>  // 等價於 type C = { a: 1; c: 3 }

是的,就是這麼簡單,好了可以來看最終的 BOSS 了

那麼最後再從 Source 中篩選出對應屬性即可,回到本文具體的例子當中,圖中紅框中的值上文已得到爲 type MyType = 'a' | 'b',那最後 Pick 一下就好了

interface Example {
 a: string;
  b: string;
  c: number;
  d: boolean;
}

// 上文得到的結果
type MyType = 'a' | 'b'

type Result = Pick<Example, MyType>  // 等價於 type Result = { a: string; b: string }

// ---- 以上等價於 ---- //

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}
type NewType = FilterConditionally<Sample, string>
/*
 NewType 最終結果爲:
 {
  a: string;
  b: string
 }
*/

這就是文章開頭的結果獲取的全過程

實戰應用例子

正如本文標題所說的,TS 類型過濾在很多優秀的開源庫中是非常常見的,比如我們熟悉的 React 中就是:

type ElementType<P = any> = {
 [K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K] ? K : never
}[keyof JSX.IntrinsicElements] | ComponentType<P>;

最後

開源庫中像 TS 類型過濾這種場景太多太多了,希望今後大家遇到時能輕鬆讀懂。如果在屏幕前閱讀的你是後端,說不定也能在後端的開源框架源碼中看到它的身影呢~

參考資料

[1]

TS 文檔——泛型: https://www.tslang.cn/docs/handbook/generics.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EkcFlwTu6u9gC-8nvU0olQ