索引型別、對映型別與條件型別_TypeScript筆記12
一.索引型別(Index types)
索引型別讓靜態檢查能夠覆蓋到型別不確定(無法窮舉)的”動態“場景,例如:
function pluck(o, names) { return names.map(n => o[n]); }
pluck
函式能從o
中摘出來names
指定的那部分屬性,存在2個型別約束:
-
引數
names
中只能出現o
身上有的屬性 -
返回型別取決於引數
o
身上屬性值的型別
這兩條約束都可以通過泛型來描述:
interface pluck { <T, K extends keyof T>(o: T, names: K[]): T[K][] } let obj = { a: 1, b: '2', c: false }; // 引數檢查 // 錯誤 Type 'string' is not assignable to type '"a" | "b" | "c"'. pluck(obj, ['n']); // 返回型別推斷 let xs: (string | number)[] = pluck(obj, ['a', 'b']);
P.S.interface
能夠描述函式型別,具體見二.函式
出現了2個新東西:
-
keyof
:索引型別查詢操作符(index type query operator) -
T[K]
:索引訪問操作符(indexed access operator):
索引型別查詢操作符
keyof T
取型別T
上的所有public
屬性名構成聯合型別,例如:
// 等價於 let t: { a: number; b: string; c: boolean; } let t: typeof obj; // 等價於 let availableKeys: "a" | "b" | "c" let availableKeys: keyof typeof obj; declare class Person { private married: boolean; public name: string; public age: number; } // 等價於 let publicKeys: "name" | "age" let publicKeys: keyof Person;
P.S.注意,不同於typeof
面向值,
keyof
是針對型別的
,而不是值(因此keyof obj
不合法)
這種型別查詢能力在pluck
等預先無法得知(或無法窮舉)屬性名的場景很有意義
索引訪問操作符
與keyof
類似,另一種型別查詢能力是按索引訪問型別(T[K]
),相當於型別層面的屬性訪問操作符
:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] } let c: boolean = getProperty(obj, 'c'); // 等價於 let cValue: typeof obj['c'] = obj['c'];
也就是說,如果t: T
、k: K
,那麼t[k]: T[K]
:
type typesof<T, K extends keyof T> = T[K]; let a: typesof<typeof obj, 'a'> = obj['a']; let bOrC: typesof<typeof obj, 'b' | 'c'> = obj['b']; bOrC = obj['c']; // 錯誤 Type 'number' is not assignable to type 'string | boolean'. bOrC = obj['a'];
索引型別與字串索引簽名
keyof
與T[K]
同樣適用於字串索引簽名(index signature)
,例如:
interface NetCache { [propName: string]: object; } // string | number 型別 let keyType: keyof NetCache; // object 型別 let cached: typesof<NetCache, 'http://example.com'>;
注意到keyType
的型別是string | number
,而不是預期的string
,這是因為在JavaScript裡的數值索引會被轉換成字串索引,例如:
let netCache: NetCache; netCache[20190101] === netCache['20190101']
也就是說,key
的型別可以是字串也可以是數值,即string | number
。如果非要剔除number
的話,可以通過內建的Extract
類型別名來完成:
/** * Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never;
(摘自TypeScript/lib/lib.es5.d.ts )
let stringKey: Extract<keyof NetCache, string> = 'http://example.com';
當然,一般沒有必要這樣做,因為從型別角度來看,key: string | number
是合理的
P.S.更多相關討論,見Keyof inferring string | number when key is only a string
二.對映型別
與索引型別類似,另一種從現有型別衍生新型別的方式是做對映:
In a mapped type, the new type transforms each property in the old type in the same way.
例如:
type Stringify<T> = { [P in keyof T]: string } // 把所有屬性值都toString()一遍 function toString<T>(obj: T): Stringify<T> { return Object.keys(obj) .reduce((a, k) => ({ ...a, [k]: obj[k].toString() }), Object.create(null) ); } let stringified = toString({ a: 1, b: 2 }); // 錯誤 Type 'number' is not assignable to type 'string'. stringified = { a: 1 };
Stringify
實現了{ [propName: string]: any }
到{ [propName: string]: string }
的型別對映,但看起來不那麼十分有用。實際上,更常見的用法是通過對映型別來改變key
的屬性,比如把一個型別的所有屬性都變成可選或只讀:
type Partial<T> = { [P in keyof T]?: T[P]; } type Readonly<T> = { readonly [P in keyof T]: T[P]; }
(摘自TypeScript/lib/lib.es5.d.ts )
let obj = { a: 1, b: '2' }; let constObj: Readonly<typeof obj>; let optionalObj: Partial<typeof obj>; // 錯誤 Cannot assign to 'a' because it is a read-only property. constObj.a = 2; // 錯誤 Type '{}' is missing the following properties from type '{ a: number; b: string; }': a, b obj = {}; // 正確 optionalObj = {};
語法格式
最直觀的例子:
// 找一個“型別集” type Keys = 'a' | 'b'; // 通過型別對映得到新型別 { a: boolean, b: boolean } type Flags = { [K in Keys]: boolean };
[K in Keys]
形式上與索引簽名類似,只是融合了for...in
語法。其中:
-
K
:型別變數,依次繫結到每個屬性上,對應每個屬性名的型別 -
Keys
:字串字面量構成的聯合型別,表示一組屬性名(的型別) -
boolean
:對映結果型別,即每個屬性值的型別
類似的,[P in keyof T]
只是找keyof T
作為(屬性名)型別集,從而對現有型別做對映得到新型別
P.S.另外,Partial
與Readonly
都能夠完整保留源型別資訊(從輸入的源型別中取屬性名及值型別,僅存在修飾符上的差異,源型別與新型別之間有相容關係),稱為同態(homomorphic)
轉換,而Stringify
丟棄了源屬性值型別,屬於非同態(non-homomorphic)轉換
“拆箱”推斷(unwrapping inference)
對型別做對映相當於型別層面的“裝箱” :
// 包裝型別 type Proxy<T> = { get(): T; set(value: T): void; } // 裝箱(普通型別 to 包裝型別的型別對映) type Proxify<T> = { [P in keyof T]: Proxy<T[P]>; } // 裝箱函式 function proxify<T>(o: T): Proxify<T> { let result: Proxify<T>; // ... wrap proxies ... return result; }
例如:
// 普通型別 interface Person { name: string, age: number } let lily: Person; // 裝箱 let proxyProps: Proxify<Person> = proxify(lily);
同樣,也能“拆箱”:
function unproxify<T>(t: Proxify<T>): T { let result = {} as T; for (const k in t) { result[k] = t[k].get(); } return result; } let originalProps: Person = unproxify(proxyProps);
能夠自動推斷出最後一行的unproxify
函式型別為:
function unproxify<Person>(t: Proxify<Person>): Person
從引數型別proxyProps: Proxify<Person>
中取出了Person
作為返回值型別,即所謂“拆箱”
三.條件型別
條件型別用來表達非均勻型別對映(non-uniform type mapping),能夠根據型別相容關係(即條件 )從兩個型別中選出一個:
T extends U ? X : Y
語義類似於三目運算子,若T
是U
的子型別,則為X
型別,否則就是Y
型別。另外,還有一種情況是條件的真假無法確定(無法確定T
是不是U
的子型別),此時為X | Y
型別,例如:
declare function f<T extends boolean>(x: T): T extends true ? string : number; // x 的型別為 string | number let x = f(Math.random() < 0.5)
另外,如果T
或U
含有型別變數,就要等到型別變數都有對應的具體型別後才能得出條件型別的結果:
When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not a the type system has enough information to conclude that T is always assignable to U.
例如:
interface Foo { propA: boolean; propB: boolean; } declare function f<T>(x: T): T extends Foo ? string : number; function foo<U>(x: U) { // a 的型別為 U extends Foo ? string : number let a = f(x); let b: string | number = a; }
其中a
的型別為U extends Foo ? string : number
(即條件不確定的情況),因為f(x)
中x
的型別U
尚不確定,無從得知U
是不是Foo
的子型別。但條件型別無非兩種可能型別,所以let b: string | number = a;
一定是合法的(無論x
是什麼型別)
可分配條件型別
可分配條件型別(distributive conditional type)中被檢查的型別是個裸型別引數(naked type parameter)。其特殊之處在於滿足分配律:
(A | B | C) extends U ? X : Y 等價於 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
例如:
// 巢狀的條件型別類似於模式匹配 type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; // T 型別等價於聯合型別 string" | "function type T = TypeName<string | (() => void)>;
另外,在T extends U ? X : Y
中,X
中出現的T
都具有U
型別約束:
type BoxedValue<T> = { value: T }; type BoxedArray<T> = { array: T[] }; type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>; // T 型別等價於聯合型別 BoxedValue<string> | BoxedArray<boolean> type T = Boxed<string | boolean[]>;
上例中Boxed<T>
的True分支具有any[]
型別約束,因此能夠通過索引訪問(T[number]
)得到陣列元素的型別
應用場景
條件型別結合對映型別能夠實現具有針對性的型別對映(不同源型別能夠對應不同的對映規則),例如:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; // 摘出所有函式型別的屬性 type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>; interface Part { id: number; name: string; subparts: Part[]; updatePart(newName: string): void; } // T 型別等價於字串字面量型別 "updatePart" type T = FunctionPropertyNames<Part>;
而可分配條件型別通常用來篩選聯合型別:
type Diff<T, U> = T extends U ? never : T; // T 型別等價於聯合型別 "b" | "d" type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // 更進一步的 type NeverNullable<T> = Diff<T, null | undefined>; function f1<T>(x: T, y: NeverNullable<T>) { x = y; // 錯誤 Type 'T' is not assignable to type 'Diff<T, null>'. y = x; }
條件型別中的型別推斷
在條件型別的extends
子句中,可以通過infer
宣告引入一個將被推斷的型別變數,例如:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
上例中引入了型別變數R
表示函式返回型別,並在True分支中引用,從而提取出返回型別
P.S.特殊的,如果存在過載,就取最後一個簽名(按照慣例,最後一個通常是最寬泛的)進行推斷,例如:
declare function foo(x: string): number; declare function foo(x: number): string; declare function foo(x: string | number): string | number; // T 型別等價於聯合型別 string | number type T = ReturnType<typeof foo>;
P.S.更多示例見Type inference in conditional types
預定義的條件型別
TypeScript 還內建了一些常用的條件型別:
// 從 T 中去掉屬於 U 的子型別的部分,即之前示例中的 Diff type Exclude<T, U> = T extends U ? never : T; // 從 T 中篩選出屬於 U 的子型別的部分,之前示例中的 Filter type Extract<T, U> = T extends U ? T : never; // 從 T 中去掉 null 與 undefined 部分 type NonNullable<T> = T extends null | undefined ? never : T; // 取出函式型別的返回型別 type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; // 取出建構函式型別的示例型別 type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
(摘自TypeScript/lib/lib.es5.d.ts )
具體示例見Predefined conditional types
四.總結
除型別組合外,另2種產生新型別的方式是型別查詢與型別對映
型別查詢:
- 索引型別:取現有型別的一部分產生新型別
型別對映:
- 對映型別:對現有型別做對映得到新型別
- 條件型別:允許以型別相容關係為條件進行簡單的三目運算,用來表達非均勻型別對映