跳到主要內容
黯羽輕揚每天積累一點點

索引型別、對映型別與條件型別_TypeScript 筆記 12

免費2019-03-23#TypeScript#TypeScript type mapping#TypeScript type query#TypeScript conditional type#TypeScript Distributive conditional types#可分配条件类型

除型別組合外,另 2 種產生新型別的方式是型別查詢與型別對映

一。索引型別(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: Tk: 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'];

索引型別與字串索引簽名

keyofT[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. 另外,PartialReadonly 都能夠完整保留源型別資訊(從輸入的源型別中取屬性名及值型別,僅存在修飾符���的差異,源型別與新型別之間有相容關係),稱為 同態(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

語義類似於三目運算子,若 TU 的子型別,則為 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)

另外,如果 TU 含有型別變數,就要等到型別變數都有對應的具體型別後才能得出條件型別的結果:

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 種產生新型別的方式是型別查詢與型別對映

型別查詢:

  • 索引型別:取現有型別的一部分產生新型別

型別對映:

  • 對映型別:對現有型別做對映得到新型別
  • 條件型別:允許以型別相容關係為條件進行簡單的三目運算,用來表達非均勻型別對映

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論