TypeScript高級類型-條件類型
預備知識:
爲什麼需要條件類型?
在TypeScript使用過程中,我們一般會直接指定具體類型
比如:
let str: string = 'test';
然而,我們在編寫代碼的過程中,會遇到不能明確指定其具體類型的情況
比如:
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// Type is 'string | number'
let x = f(Math.random() < 0.5)
// Type is 'number'
let y = f(false)
// Type is 'string'
let z = f(true)
在編寫函數 f
時,只知道返回值的範圍,但不知道其具體類型,其具體類型需要等到函數執行時進行確定,換句話說,只有類型系統中給出 充足的條件
之後,它纔會根據條件推斷出類型結果。
條件類型是什麼及其使用
先看一下條件類型是什麼
T extends U ? X : Y
上面的類型表示:若 T
能夠分配(賦值)給 U
,那麼類型是 X
,否則爲 Y
,有點類似於JavaScript中的三元條件運算符。
上文說到只有類型系統中給出 充足的條件
之後,它纔會根據條件推斷出類型結果,如果判斷條件不足,則會得到第三種結果,即 推遲
條件判斷,等待充足條件。
例如:
interface Foo {
propA: boolean;
propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
// 因爲 ”x“ 未知,因此判斷條件不足,不能確定條件分支,推遲條件判斷直到 ”x“ 明確,
// 推遲過程中,”a“ 的類型爲分支條件類型組成的聯合類型,
// string | number
let a = f(x);
// 這麼做是完全可以的
let b: string | number = a;
}
條件類型經常用於TypeScript類型編程當中,在高級類型編寫時會經常見到它的身影
比如:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
後面文章會逐漸講解到!
條件類型與聯合類型
分佈式條件類型
什麼樣的條件類型稱爲分佈式條件類型呢?
答案是:條件類型裏待檢查的類型必須是裸類型(naked type parameter
)
到目前爲止,我們可以捕獲到兩個疑問點
- 什麼類型是裸類型?
- 分佈式如何理解?
先看什麼類型是裸類型
裸類型是指類型參數沒有被包裝在其他類型裏,比如沒有被數組、元組、函數、Promise等等包裹,簡而言之裸類型就是未經過任何其他類型修飾或包裝的類型。
比如:
// 裸類型參數,沒有被任何其他類型包裹,即T
type NakedType<T> = T extends boolean ? "YES" : "NO"
// 類型參數被包裹的在元組內,即[T]
type WrappedType<T> = [T] extends [boolean] ? "YES" : "NO";
分佈式如何理解
分佈式條件類型在實例化時會自動分發成聯合類型
什麼意思呢?
例如,T extends U ? X : Y
使用類型參數A | B | C
實例化 T
解析爲 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
結合 乘法分配律
理解一下!
接下來結合具體實例我們來看一下分佈式條件類型
與 不含有分佈式特性的條件類型
// 含有分佈式特性的,待檢查類型必須爲”裸類型“
type Distributed = NakedType<number | boolean> // = NakedType<number> | NakedType<boolean> = "NO" | "YES"(結合一下乘法分配律便於理解與記憶哦~)
// 不含有分佈式特性的,待檢查的類型爲包裝或修飾過的類型
type NotDistributed = WrappedType<number | boolean > // "NO"
搞明白了分佈式條件類型,我們編寫這樣一個類型工具 NonNullable<T>
,即從類型 T
中排除 null 和 undefined
,我們期待的結果如下:
type a = NonNullable<string | number | undefined | null> // 得到type a = string | number
藉助條件類型可以很容易寫出來
type NonNullable<T> = T extends null | undefined ? never : T
注意:
never
類型表示不會是任何值,即什麼都沒有
條件類型與映射類型
條件類型與映射類型的結合經常會被作爲考點,常見題型多爲設計類型工具方法
映射類型相關內容見 TypeScript高級類型-Partial分析
接下來設計一個這樣的類型工具NonFunctionKeys<T>
,通過使用 NonFunctionKeys<T>
得到對象類型 T
中非函數的屬性名組成的聯合類型
type MixedProps = { name: string; setName: (name: string) => void };
// Expect: "name"
type Keys = NonFunctionKeys<MixedProps>;
那麼如何設計呢?
- 使用JavaScript表述出來
- 遍歷
MixedProps
的key,value
, - 找出每個
value
是否是函數類型,是則排除掉,否則保留(ts中保留的爲對應的key,這樣方便使用索引訪問操作符取出) - 取出所有保留的
key
- 遍歷
- 使用TypeScript進行實現
type javascript<T> = {
[P in keyof T]: T[P] extends Function ? never : P // 這裏保留的value被替換爲了key
}[keyof T]
裏面設計到的
keyof
、in
、T[P]
可參考 TypeScript高級類型-Partial分析
條件類型中的類型推斷
在 extends
條件類型的子句中,現在可以含有 infer
引入要推斷的類型變量的聲明,可以在條件類型的真實分支中引用此類推斷的類型變量,另外,infer
同一類型變量可能有多個位置。
簡單而言 infer 關鍵字就是聲明一個類型變量,當類型系統給足條件的時候類型就會被推斷出來。
例如,以下代碼提取函數類型的返回類型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
下面的示例演示在協變位置上同一類型變量的多個候選類型將會被推斷爲聯合類型:
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type t1 = Foo<{ a: string, b: string }>; // string
type t2 = Foo<{ a: string, b: number }>; // string | number
同樣在逆變位置上同一類型變量的多個候選類型將會被推斷爲交叉類型:
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type t1 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type t2 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
注意:
infer
對於常規類型參數(泛型約束),不能在約束子句中使用infer
聲明
比如:
type ReturnType<T extends (...args: any[]) => infer R> = R; // Error, not supported
下面我們實現一下 ConstructorParameters<T>
,用於提取構造函數中參數類型
class TestClass {
constructor(public name: string, public age: number) {}
}
// 期待結果如下
type paramsType = ConstructorParameters<typeof TestClass> // [string, number]
- 拿到
TestClass
構造函數簽名 - 使用 infer 推斷構造函數的入參類型
type ConstructorParameters<T extends new (...args: any[]) => any> =
T extends new (...args: infer P) => any ? P : never;
重點:
-
new (...args: any[]
指構造函數 -
infer P
代表待推斷的構造函數參數,如果接受的類型T
是一個構造函數,那麼返回構造函數的參數類型P
,否則什麼也不返回,即never
類型
infer 的應用也是非常廣泛的
比如:
tuple 轉 union,[string, number] -> string | number
type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number];
type ToUnion = ElementOf<TTuple>; // string | number
union 轉 intersection,如:string | number -> string & number
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Result = UnionToIntersection<string | number>; // 注意:string & number 就是 never
重點:
-
U extends any
是具有分佈式有條件類型特性,因爲待檢查類型U
爲裸類型 -
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
最後一個 extends 前面作爲待檢查類型,因爲被函數包裝,因此不具有分佈式有條件類型特性-
type UnionToIntersection<U> = ((k: string) => void | (k: number) => void) extends ((k: infer I) => void) ? I : never;
-
根據
逆變特性
推斷出的I
應該具備string 和 number
的類型,故爲交叉類型string & number
,而該交叉類型在vscode
中表現爲never
-
本文重點:
- 需要明確條件類型的表達方式,即
T extends U ? X : Y
- 需要明確什麼是分佈式有條件類型,以及判斷爲分佈式有條件類型的前提條件
- 分佈式
- 裸類型
- infer關鍵字,明確它的使用範圍及其作用