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

晨曦時夢見兮發表於2020-02-05

TypeScript中有一項相當重要的進階特性:conditional types,這個功能出現以後,很多積壓已久的TypeScript功能都可以輕而易舉的實現了。

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

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

這樣的函式呼叫方式給簡化為:

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

沒錯,它只是節省了幾個字串,但是卻是一個非常適合我們深入學習條件型別的實戰。

通過這篇文章,你可以學到以下特性在實戰中是如何使用的:

  1. ?TypeScript的高階型別(Advanced Type
  2. ?Conditional Types (條件型別)
  3. ?Distributive conditional types (分佈條件型別)
  4. ?Mapped types(對映型別)
  5. ?函式過載

conditional types的第一次使用

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

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

這樣的語法就叫做條件型別,A, B, CD可以是任何型別表示式。

可分配性

這個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語法,並且利用內建型別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…

相關文章