TypeScript 條件類型精讀與實踐

在大多數程序中,我們必須根據輸入做出決策。TypeScript 也不例外,使用條件類型可以描述輸入類型與輸出類型之間的關系。

本文同步首發在個人博客中,歡迎訂閱、交流。

用于條件判斷時的 extends

當 extends 用于表示條件判斷時,可以總結出以下規律

  1. 若位于 extends 兩側的類型相同,則 extends 在語義上可理解為 ===,可以參考如下例子:
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false     // false
  1. 若位于 extends 右側的類型包含位于 extends 左側的類型(即狹窄類型 extends 寬泛類型)時,結果為 true,反之為 false。可以參考如下例子:
type result3 = string extends string | number ? true : false // true
  1. 當 extends 作用于對象時,若在對象中指定的 key 越多,則其類型定義的范圍越狹窄。可以參考如下例子:
type result4 = { a: true, b: false } extends { a: true } ? true : false // true

在泛型類型中使用條件類型

考慮如下 Demo 類型定義:

type Demo<T, U> = T extends U ? never : T

結合用于條件判斷時的 extends,可知 'a' | 'b' | 'c' extends 'a' 是 false, 因此 Demo<'a' | 'b' | 'c', 'a'> 結果是 'a' | 'b' | 'c' 么?

查閱官網,其中有提到:

When conditional types act on a generic type, they become distributive when given a union type.

即當條件類型作用于泛型類型時,聯合類型會被拆分使用。即 Demo<'a' | 'b' | 'c', 'a'> 會被拆分為 'a' extends 'a''b' extends 'a''c' extends 'a'。用偽代碼表示類似于:

function Demo(T, U) {
  return T.map(val => {
    if (val !== U) return val
    return 'never'
  })
}

Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']

此外根據 never 類型的定義 —— never 類型可分配給每種類型,但是沒有類型可以分配給 never(除了 never 本身)。即 never | 'b' | 'c' 等價于 'b' | 'c'

因此 Demo<'a' | 'b' | 'c', 'a'> 的結果并不是 'a' | 'b' | 'c' 而是 'b' | 'c'

工具類型

心細的讀者可能已經發現了 Demo 類型的聲明過程其實就是 TypeScript 官方提供的工具類型中 Exclude<Type, ExcludedUnion> 的實現原理,其用于將聯合類型 ExcludedUnion 排除在 Type 類型之外。

type T = Demo<'a' | 'b' | 'c', 'a'> // T: 'b' | 'c'

基于 Demo 類型定義,進一步地還可以實現官方工具類型中的 Omit<Type, Keys>,其用于移除對象 Type
中滿足 keys 類型的屬性值。

type Omit<Type, Keys> = {
  [P in Demo<keyof Type, Keys>]: Type<P>
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type T = Omit<Todo, 'description'> // T: { title: string; completed: boolean }

逃離艙

如果想讓 Demo<'a' | 'b' | 'c', 'a'> 的結果為 'a' | 'b' | 'c' 是否可以實現呢? 根據官網描述:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

如果不想遍歷泛型中的每一個類型,可以用方括號將泛型給括起來以表示使用該泛型的整體部分。

type Demo<T, U> = [T] extends [U] ? never : T

// result 此時類型為 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>

在箭頭函數中使用條件類型

在箭頭函數中使用三元表達式時,從左向右的閱讀習慣導致函數內容區若不加括號則會讓使用方感到困惑。比如下方代碼中 x 是函數類型還是布爾類型呢?

// The intent is not clear.
var x = a => 1 ? true : false

在 eslint 規則 no-confusing-arrow 中,推薦如下寫法:

var x = a => (1 ? true : false)

在 TypeScript 的類型定義中,若在箭頭函數中使用 extends 也是同理,由于從左向右的閱讀習慣,也會導致閱讀者對類型代碼的執行順序感到困惑。

type Curry<P extends any[], R> =
  (arg: Head<P>) => HasTail<P> extends true ? Curry<Tail<P>, R> : R

因此在箭頭函數中使用 extends 建議加上括號,對于進行 code review 有很大的幫助。

type Curry<P extends any[], R> =
  (arg: Head<P>) => (HasTail<P> extends true ? Curry<Tail<P>, R> : R)

結合類型推導使用條件類型

在 TypeScript 中,一般會結合 extends 來使用類型推導 infer 語法。使用它可以實現自動推導類型的目的。比如用其來實現工具類型 ReturnType<Type>,該工具類型用于返回函數 Type 的返回類型。

type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

MyReturnType<() => string>          // string
MyReturnType<() => Promise<boolean> // Promise<boolean>

結合 extends 與類型推導還可以實現與數組相關的 Pop<T>Shift<T>Reverse<T> 工具類型。

Pop<T>:

type Pop<T extends any[]> = T extends [...infer ExceptLast, any] ? ExceptLast : never

type T = Pop<[3, 2, 1]> // T: [3, 2]

Shift<T>:

type Shift<T extends any[]> = T extends [infer _, ...infer O] ? O : never

type T = Shift<[3, 2, 1]> // T: [2, 1]

Reverse<T>

type Reverse<T> = T extends [infer F, ...infer Others]
  ? [...Reverse<Others>, F]
  : []

type T = Reverse<['a', 'b']> // T: ['b', 'a']

使用條件類型來判斷兩個類型完全相等

我們也可以使用條件類型來判斷 A、B 兩個類型是否完全相等。當前社區上主要有兩種方案:

方案一: 參考 issue

export type Equal1<T, S> =
  [T] extends [S] ? (
    [S] extends [T] ? true : false
  ) : false

目前該方案的唯一缺點是會將 any 類型與其它任何類型判為相等。

type T = Equal1<{x:any}, {x:number}> // T: true

方案二: 參考 issue

export type Equal2<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<U>() => U extends Y ? 1 : 2) ? true : false

目前該方案的唯一缺點是在對交叉類型的處理上有一點瑕疵。

type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // false

以上兩種判斷類型相等的方法見仁見智,筆者在此拋磚引玉。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容