TypeScript中有一項相當重要的進階特性:conditional types
,這個功能出現以后,很多積壓已久的TypeScript功能都可以輕而易舉的實現了。
那么本篇文章就會通過一個簡單的功能:把
distribute({ type: 'LOGIN', email: string })
這樣的函數調用方式給簡化為:
distribute('LOGIN', { email: string })
沒錯,它只是節省了幾個字符串,但是卻是一個非常適合我們深入學習條件類型的實戰。
通過這篇文章,你可以學到以下特性在實戰中是如何使用的:
- 🎉TypeScript的高級類型(Advanced Type)
- 🎉Conditional Types (條件類型)
- 🎉Distributive conditional types (分布條件類型)
- 🎉Mapped types(映射類型)
- 🎉函數重載
conditional types的第一次使用
先簡單的看一個條件類型的示例:
function process<T extends string | null>( text: T ): T extends string ? string : null { ... }
A extends B ? C : D
這樣的語法就叫做條件類型,A
, B
, C
和D
可以是任何類型表達式。
可分配性
這個extends
關鍵字是條件類型的核心。 A extends B
恰好意味着可以將類型A的任何值安全地分配給類型B的變量。在類型系統術語中,我們可以說“ A可分配給B”。
從結構上來講,我們可以說A extends B
,就像“ A是B的超集”,或者更確切地說,“ A具有B的所有特性,也許更多”。
舉個例子來說 { foo: number, bar: string } extends { foo: number }
是成立的,因為前者顯然是后者的超集,比后者擁有更具體的類型。
分布條件類型
官方文檔中,介紹了一種操作,叫 Distributive conditional types
簡單來說,傳入給T extends U
中的T
如果是一個聯合類型A | B | C
,則這個表達式會被展開成
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
條件類型讓你可以過濾聯合類型的特定成員。 為了說明這一點,假設我們有一個稱為Animal的聯合類型:
type Animal = Lion | Zebra | Tiger | Shark
再假設我們要編寫一個類型,來過濾出Animal中屬於“貓”的那些類型
type ExtractCat<A> = A extends { meow(): void } ? A : never type Cat = ExtractCat<Animal> // => Lion | Tiger
接下來,Cat的計算過程會是這樣子的:
type Cat = | ExtractCat<Lion> | ExtractCat<Zebra> | ExtractCat<Tiger> | ExtractCat<Shark>
然后,它被計算成聯合類型
type Cat = Lion | never | Tiger | never
然后,聯合類型中的never沒什么意義,所以最后的結果的出來了:
type Cat = Lion | Tiger
記住這樣的計算過程,記住ts這個把聯合類型如何分配給條件類型,接下來的實戰中會很有用。
分布條件類型的真實用例
舉一個類似redux
中的dispatch
的例子。
首先,我們有一個聯合類型Action
,用來表示所有可以被dispatch接受的參數類型:
type Action = | { type: "INIT" } | { type: "SYNC" } | { type: "LOG_IN" emailAddress: string } | { type: "LOG_IN_SUCCESS" accessToken: string }
然后我們定義這個dispatch方法:
declare function dispatch(action: Action): void // ok dispatch({ type: "INIT" }) // ok dispatch({ type: "LOG_IN", emailAddress: "david.sheldrick@artsy.net" }) // ok dispatch({ type: "LOG_IN_SUCCESS", accessToken: "038fh239h923908h" })
這個API是類型安全的,當TS識別到type為LOG_IN
的時候,它會要求你在參數中傳入emailAddress
這個參數,這樣才能完全滿足聯合類型中的其中一項。
到此為止,我們可以去和女朋友約會了,此文完結。
等等,我們好像可以讓這個api變得更簡單一點:
dispatch("LOG_IN_SUCCESS", { accessToken: "038fh239h923908h" })
好,推掉我們的約會,打電話給我們的女朋友!取消!
參數簡化實現
首先,利用方括號選擇出Action
中的所有type
,這個技巧很有用。
type ActionType = Action["type"] // => "INIT" | "SYNC" | "LOG_IN" | "LOG_IN_SUCCESS"
但是第二個參數的類型取決於第一個參數。 我們可以使用類型變量來對該依賴關系建模。
declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParameters<Action, T> ): void
注意,這里就用到了extends
語法,規定了我們的入參type
必須是ActionType
中一部分。
注意這里的第二個參數args,用ExtractActionParameters<Action, T>
這個類型來把type和args做了關聯,
來看看ExtractActionParameters
是如何實現的:
type ExtractActionParameters<A, T> = A extends { type: T } ? A : never
在這次實戰中,我們第一次運用到了條件類型,ExtractActionParameters<Action, T>
會按照我們上文提到的分布條件類型
,把Action中的4項依次去和{ type: T }
進行比對,找出符合的那一項。
來看看如何使用它:
type Test = ExtractActionParameters<Action, "LOG_IN"> // => { type: "LOG_IN", emailAddress: string }
這樣就篩選出了type匹配的一項。
接下來我們要把type去掉,第一個參數已經是type了,因此我們不想再額外聲明type了。
// 把類型中key為"type"去掉 type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">]: A[K] }
這里利用了keyof
語法,並且利用內置類型Exclude
把type
這個key去掉,因此只會留下額外的參數。
type Test = ExcludeTypeField<{ type: "LOG_IN", emailAddress: string }> // { emailAddress: string }
然后用它來剔除參數中的 type
// 把參數對象中的type去掉 type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>; declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T> ): void
到此為止,我們就可以實現上文中提到的參數簡化功能:
// ok dispatch({ type: "LOG_IN", emailAddress: "david.sheldrick@artsy.net" })
利用重載進一步優化
到了這一步為止,雖然帶參數的Action可以完美支持了,但是對於"INIT"這種不需要傳參的Action,我們依然要寫下面這樣代碼:
dispatch("INIT", {}) 這肯定是不能接受的!所以我們要利用TypeScript的函數重載功能。
// 簡單參數類型 function dispatch<T extends SimpleActionType>(type: T): void // 復雜參數類型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T>, ): void // 實現 function dispatch(arg: any, payload?: any) {}
那么關鍵點就在於SimpleActionType
和ComplexActionType
要如何實現了,
SimpleActionType
顧名思義就是除了type以外不需要額外參數的Action類型,
type SimpleAction = ExtractSimpleAction<Action>
我們如何定義這個ExtractSimpleAction
條件類型?
如果我們從這個Action中刪除type
字段,並且結果是一個空的接口,
那么這就是一個SimpleAction
。 所以我們可能會憑直覺寫出這樣的代碼:
type ExtractSimpleAction<A> = ExcludeTypeField<A> extends {} ? A : never
但這樣是行不通的,幾乎所有的類型都可以extends {},因為{}太寬泛了。
我們應該反過來寫:
type ExtractSimpleAction<A> = {} extends ExcludeTypeField<A> ? A : never
現在,如果ExcludeTypeField <A>
為空,則extends表達式為true,否則為false。
但這仍然行不通! 因為分布條件類型
僅在extends關鍵字的前面是類型變量時發生。
分布條件件類型僅發生在如下場景:
type Blah<Var> = Var extends Whatever ? A : B
而不是:
type Blah<Var> = Foo<Var> extends Whatever ? A : B type Blah<Var> = Whatever extends Var ? A : B
但是我們可以通過一些小技巧繞過這個限制:
type ExtractSimpleAction<A> = A extends any ? {} extends ExcludeTypeField<A> ? A : never : never
A extends any
是一定成立的,這只是用來繞過ts對於分布條件類型的限制,沒錯啊,我們的A
確實是在extends的前面了,就是騙你TS,這里是分布條件類型。
而我們真正想要做的條件判斷被放在了中間,因此Action聯合類型中的每一項又能夠分布的去匹配了。
那么我們就可以簡單的篩選出所有不需要額外參數的type
type SimpleAction = ExtractSimpleAction<Action> type SimpleActionType = SimpleAction['type']
再利用Exclude取反,找到復雜類型:
type ComplexActionType = Exclude<ActionType, SimpleActionType>
到此為止,我們所需要的功能就完美實現了:
// 簡單參數類型 function dispatch<T extends SimpleActionType>(type: T): void // 復雜參數類型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParameters<Action, T>, ): void // 實現 function dispatch(arg: any, payload?: any) {} // ok dispatch("SYNC") // ok dispatch({ type: "LOG_IN", emailAddress: "david.sheldrick@artsy.net" })
完整代碼
type Action = | { type: "INIT"; } | { type: "SYNC"; } | { type: "LOG_IN"; emailAddress: string; } | { type: "LOG_IN_SUCCESS"; accessToken: string; }; // 用類型查詢查出Action中所有type的聯合類型 type ActionType = Action["type"]; // 把類型中key為"type"去掉 type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">]: A[K] }; type ExtractActionParameters<A, T> = A extends { type: T } ? A : never // 把參數對象中的type去掉 // Extract<A, { type: T }會挑選出能extend { type: T }這個結構的Action中的類型 type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>; type ExtractSimpleAction<A> = A extends any ? {} extends ExcludeTypeField<A> ? A : never : never; type SimpleActionType = ExtractSimpleAction<Action>["type"]; type ComplexActionType = Exclude<ActionType, SimpleActionType>; // 簡單參數類型 function dispatch<T extends SimpleActionType>(type: T): void; // 復雜參數類型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T> ): void; // 實現 function dispatch(arg: any, payload?: any) {} dispatch("SYNC"); dispatch('LOG_IN', { emailAddress: 'ssh@qq.com' })
總結
本文的實戰示例來自國外大佬的博客,我結合個人的理解整理成了這篇文章。
中間涉及到的一些進階的知識點,如果小伙伴們不太熟悉的話,可以參考各類文檔中的定義去反復研究,相信你會對TypeScript有更深一步的了解。
參考資料
源碼
這里是用TS內置工具類型改造過后的源碼,更加簡潔優雅的完成了本文中的需求,可以擴展學習。