TypeScript 參數簡化實戰(進階知識點conditional types,中高級必會)

TypeScript中有一項至關重要的進階特性:conditional types,這個功能出現之後,不少積壓已久的TypeScript功能均可以垂手可得的實現了。html

那麼本篇文章就會經過一個簡單的功能:把git

distribute({
    type: 'LOGIN',
    email: string
})
複製代碼

這樣的函數調用方式給簡化爲:github

distribute('LOGIN', {
    email: string
})
複製代碼

沒錯,它只是節省了幾個字符串,可是倒是一個很是適合咱們深刻學習條件類型的實戰。typescript

經過這篇文章,你能夠學到如下特性在實戰中是如何使用的:

  1. 🎉TypeScript的高級類型(Advanced Type
  2. 🎉Conditional Types (條件類型)
  3. 🎉Distributive conditional types (分佈條件類型)
  4. 🎉Mapped types(映射類型)
  5. 🎉函數重載

conditional types的第一次使用

先簡單的看一個條件類型的示例:redux

function process<T extends string | null>( text: T ): T extends string ? string : null {
  ...
}
複製代碼
A extends B ? C : D
複製代碼

這樣的語法就叫作條件類型,A, B, CD能夠是任何類型表達式。api

可分配性

這個extends關鍵字是條件類型的核心。 A extends B剛好意味着能夠將類型A的任何值安全地分配給類型B的變量。在類型系統術語中,咱們能夠說「 A可分配給B」。安全

從結構上來說,咱們能夠說A extends B,就像「 A是B的超集」,或者更確切地說,「 A具備B的全部特性,也許更多」。bash

舉個例子來講 { foo: number, bar: string } extends { foo: number }是成立的,由於前者顯然是後者的超集,比後者擁有更具體的類型。app

分佈條件類型

官方文檔中,介紹了一種操做,叫 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語法,而且利用內置類型Excludetype這個key去掉,所以只會留下額外的參數。

type Test = ExcludeTypeField<{ type: "LOG_IN", emailAddress: string }>
// { emailAddress: string }
複製代碼

到此爲止,咱們就能夠實現上文中提到的參數簡化功能:

// 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: ExtractActionParameters<Action, T>, ): void // 實現 function dispatch(arg: any, payload?: any) {} 複製代碼

那麼關鍵點就在於SimpleActionTypeComplexActionType要如何實現了,

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對於分佈條件類型的限制,而咱們真正想要作的條件判斷被放在了中間,所以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"
})
複製代碼

總結

本文的實戰示例來自國外大佬的博客,我結合我的的理解整理成了這篇文章。

中間涉及到的一些進階的知識點,若是小夥伴們不太熟悉的話,能夠參考各種文檔中的定義去反覆研究,相信你會對TypeScript有更深一步的瞭解。

參考資料

artsy.github.io/blog/2018/1…

源碼

github.com/sl1673495/t…

相關文章
相關標籤/搜索