# 映射类型 (mapped type )

映射类型通过迭代联合或者字面量类型来创建新的对象类型。

# in

在 JavaScript in (opens new window) 用于判断对象或原型链是否包含某属性,也用于for in 的循环语句中迭代对象的键名。TypeScript 借鉴了此概念采用 in 操作符,在属性申明的模板上迭代属性名映射新的类型。

// 创建一个对象类型, 属性 a,b 都是字符串类型
type ObjAB = {
  // in 迭代字符串字面量类型的值作为属性名,并设定属性为 string  类型
  [key in 'a' | 'b']: string;
};

// 合法, 对象包含 a,b 且为字符串类型
let objAB: ObjAB = {
  a: 'foo',
  b: 'bar'
};

除了迭代字符串字面量,也可迭代数值字面量。

//限定索引前三个元素必须为 数值
type InNumber = {
  [key in 0 | 1 | 2]: number;
};
// 合法,只限定前 3 个元素
let inNumberArr: InNumber = [1, 2, 3, 'df'];

也可迭代枚举类型,注意属性名为枚举类型标识符的值而标识符的名称。

enum EnumType {
  Foo = 'foo',
  Bar = 1,
  FooBar
}

// 注意 a 的键名为枚举类型的值而非标识符名称。
let a: { [key in EnumType]: string } = {
  foo: 'foo',
  1: 'bar',
  2: 'foo'
};

一个典型的用例是结合泛型,来动态创建类型。

// 采用泛型映射 K 类型的键名为 T 类型
type MapK<K extends string, T> = {
  readonly [key in K]: T;
};
type PageInfo = {
  title: string;
};
type Pages = 'home' | 'about';

// 迭代 Pages 字面量类型的值作为键名,并要求值为 PageInfo 类型
let pages: MapK<Pages, PageInfo> = {
  home: {
    title: 'home'
  },
  about: {
    title: 'about'
  }
};

这正是 TypeScript 内部提供的 Record<K,T> (opens new window) 的原理。

进一步阅读查看 mapped types (opens new window)

# 结合 keyof

结合索引类型查询 keyof,你可创造更多复杂的类型模板。

/**
    定义对象泛型 T 的所有属性为只读,产生一个只读的对象类型

    1. keyof T 提取泛型 T 的键名为字面量类型
    2. P in keyof T 遍历字面量类型的值
    3. readonly [P in keyof T] 修饰键名为只读
    4. T[P] 映射每个键名对应值的类型
*/
type ReadOnlyObj<T> = {
  readonly [P in keyof T]: T[P];
};

interface Obj1 {
  foo: number;
  bar: string;
}

// 限定 readOnlyObj 的对象类型为只读的 Obj1
let readOnlyObj: ReadOnlyObj<Obj1> = {
  foo: 12,
  bar: 'bar'
};

// 报错, 只读对象不允许修改
readOnlyObj.foo = 13;

# 数组或元祖使用

可以利用 in 迭代数组结合之前的操作符,可产生类似数组的 map 方法,用于创建新的类型。

type MapToPromise<T> = {
  [key in keyof T]: Promise<T[key]>;
};

type Coordinate = [number, number];

type PromiseCoordinate = MapToPromise<Coordinate>;

// 设置 promiseAll resolve 的值必须为 number 类型的 promise 数组
let promiseAll: PromiseCoordinate = [Promise.resolve(1), Promise.resolve(2)];

示例利用 in 结合索引类型,将元祖类型装换为了包含 Promise 的元祖。

延伸阅读 github issues Mapped Types (opens new window)

TIP

TypeScript 内置了一套类型模板,详见 Utility Types (opens new window)

# 映射类型修饰符

对象类型中,可通过 readonly,? 属性修饰符表示只读和可选。 在映射类型中可利用 -,+ 表示删除或者添加的只读或可选修饰符。

// 删除泛型对象的只读和可选修饰符
type Writable<T> = {
  -readonly [K in keyof T]-?: T[K];
};

interface ReadonlyObj {
  readonly foo: string;
  readonly bar?: number;
}

// 由于删除可选, bar 属性必须存在
let writeObj: Writable<ReadonlyObj> = {
  foo: 'foo',
  bar: 12
};

// 合法, 删除了只读修饰符
writeObj.foo = 'bar';
writeObj.bar = 12;

详细资料参阅 Improved control over mapped type modifiers (opens new window)

# 使用 as 重映射