索引类型、映射类型与条件类型_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: 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:字符串字面量构成的联合类型,表示一组属性名(的类型)

  • 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

四.总结

类型组合外,另2种产生新类型的方式是类型查询与类型映射

类型查询:

  • 索引类型:取现有类型的一部分产生新类型

类型映射:

  • 映射类型:对现有类型做映射得到新类型
  • 条件类型:允许以类型兼容关系为条件进行简单的三目运算,用来表达非均匀类型映射

参考资料

索引类型、映射类型与条件类型_TypeScript笔记12》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code