阅读 946

TypeScript的索引类型和映射类型

文章首发于:https://github.com/USTB-musion/fee-skills

写在前面

在ts中,索引类型和映射类型是相对复杂的内容。使用索引类型,编译器就能够检查使用了动态属性名的代码,而使用映射类型,可以将旧类型转化成新类型。下面用一篇文章来稍微深入介绍一下这两个概念。

索引类型

在实际开发中,我们经常能遇到这样的场景,在对象中获取一些属性的值,然后建立对应的集合。

let person = {
    name: 'musion',
    age: 35
}

function getValues(person: any, keys: string[]) {
    return keys.map(key => person[key])
}

console.log(getValues(person, ['name', age])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
复制代码

在上述例子中,可以看到getValues(persion, ['gender'])打印出来的是[undefined],但是ts编译器并没有给出报错信息,那么如何使用ts对这种模式进行类型约束呢?这里就要用到了索引类型,改造一下getValues函数,通过 索引类型查询索引访问 操作符:

function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
  return keys.map(key => person[key]);
}

interface Person {
    name: string;
    age: number;
}

const person: Person = {
    name: 'musion',
    age: 35
}

getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
复制代码

编译器会检查传入的值是否是Person的一部分。通过介绍几个概念来理解上面的代码:

1.索引类型查询操作符(keyof T)

keyof T的含义表示类型T所有公共属性的字面量的联合类型,在上述例子中,就是Person的属性名,即['name', age]

2.索引访问操作符(T[K])

T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组

3.泛型约束(K extends T)

泛型变量可以通过继承某些类型获取某些属性

介绍完这三个概念之后,应该就可以理解上面的代码了。首先看泛型,这里有T和K两种类型,根据类型推断,第一个参数person就是person,类型会被推断为Person。而第二个数组参数的类型推断(K extends keyof T),keyof关键字可以获取T,也就是Person的所有属性名,即['name', 'age']。而extends关键字让泛型K继承了Person的所有属性名,即['name', 'age']。这三个特性组合保证了代码的动态性和准确性,也让代码提示变得更加丰富了:

getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
复制代码

映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

interface PersonPartial {
    name?: string;
    age?: number;
}
复制代码

在实例化Person时,我们不必给每个属性都赋值,想要一个只读版本:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}
复制代码

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。TS内置了一些映射类型(实际上是一些语法糖),让我们可以方便地进行类型映射,可以在TypeScript包中的typescript/lib/lib.es5.d.ts中找到他们的定义: 举一些例子:

// 将传入的属性变为只读选项
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
// 将传入的属性变为可选项:keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值.
type Partial<T> = {
    [P in keyof T]?: T[P];
}
复制代码

使用方式如下:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
复制代码

TypeScript内置了Readonly和Partial,所以不需要手动声明实现。除此之外,还有其他常用的内置类型如:

Record

将K中的所有属性的值转化为T类型:

type Record<K extends keyof any, T> = { [P in K]: T };
复制代码

Pick

从 T 中取出 一系列 K 的属性

type Pick<T, K extends keyof T> = { [P in K]: T[P] };
复制代码

Required

将传入的属性变为必选项

type Required<T> = { [P in keyof T]-?: T[P] };
复制代码

参考链接

Typescript高级类型

你可以关注我的公众号「慕晨同学」,鹅厂码农,平常记录一些鸡毛蒜皮的点滴,技术,生活,感悟,一起成长。