前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >索引类型、映射类型与条件类型_TypeScript笔记12

索引类型、映射类型与条件类型_TypeScript笔记12

作者头像
ayqy贾杰
发布2019-06-12 15:13:40
1.6K0
发布2019-06-12 15:13:40
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬黯羽轻扬

一.索引类型(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种产生新类型的方式是类型查询与类型映射

类型查询:

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

类型映射:

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

参考资料

  • Advanced Types
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.索引类型(Index types)
    • 索引类型查询操作符
      • 索引访问操作符
        • 索引类型与字符串索引签名
        • 二.映射类型
          • 语法格式
            • “拆箱”推断(unwrapping inference)
            • 三.条件类型
              • 可分配条件类型
                • 应用场景
              • 条件类型中的类型推断
                • 预定义的条件类型
                • 四.总结
                  • 参考资料
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档