一。索引型別(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:字串字面量構成的 [聯成型別](/articles/組合型別與型別保護-typescript 筆記 9/#articleHeader3),表示一組屬性名(的型別) -
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
四。總結
除 [型別組合](/articles/組合型別與型別保護-typescript 筆記 9/#articleHeader1) 外,另 2 種產生新型別的方式是型別查詢與型別對映
型別查詢:
- 索引型別:取現有型別的一部分產生新型別
型別對映:
- 對映型別:對現有型別做對映得到新型別
- 條件型別:允許以型別相容關係為條件進行簡單的三目運算,用來表達非均勻型別對映
暫無評論,快來發表你的看法吧