Skip to content

索引类型(Index types)

TypeScript 中的索引类型(Indexed Types)是一种高级类型,允许我们以更灵活和动态的方式定义和操作类型。 它其实包含三个部分:索引访问,索引签名索引类型查询索引类型允许我们通过索引去访问对象属性的类型,常见的形式有:

  1. 索引访问类型(Indexed Access Types): **obj[key]**
  2. 索引签名(Index Signatures): **{ [key: Type]: ValueType }**
  3. 映射类型(Mapped Types): **{ [P in Keys]: Type }**

索引访问 type[key]

使用索引访问语法(**type[key]**)可以访问属性的类型。 本质就是通过键名,去访问这个键对应的键值的类型

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

type PersonNameType = Person['name']; // 'string'
type PersonAgeType = Person['age']; // PersonAgeType类型为 number


console.log(PersonAgeType); // 注意,类型是不能作为值来log的

这种形式在编写类型守卫函数时非常有用:

typescript
function ensureString(val: any): val is string {
  return typeof val === 'string';
}

// 泛型函数narrow,至少有一个参数是泛型参数T,
function narrow<T>(obj: T, key: string): T[keyof T] {
  if (ensureString(obj[key])) {
    // 这里 obj[key] 被缩小为 string 类型
    return obj[key] as T[keyof T];
  }
  // ...
  throw new Error(`Value at key '${key}' is not a string.`);
}

eg.

typescript
interface MyObject {
  name: string
  age: number
  email: string
}

const myObj: MyObject = {
  name: 'demo',
  age: 18,
  email: 'qq.com'
}

function ensureString(value: any): value is string {
  return typeof value === 'string'
}

function narrow<T>(obj: T, key: string): string {
  if (ensureString(obj[key])) {
    return obj[key] as string
  }
  throw new Error(`Value at key '${key}' is not a string.`)
}

// 使用泛型函数 narrow

const nameTest: string = narrow(myObj, 'name') // 这里 T 将会推断为 MyObject
console.log(nameTest) // 输出: "Alice"

const email: string = narrow(myObj, 'email')
console.log(email) // 输出: "alice@example.com"

const age: number = narrow(myObj, 'age') // 这里会导致类型错误,因为 narrow 函数保证返回的是字符串类型

索引签名

**{ [key: Type]: ValueType }** 索引签名用于定义一个可以通过索引访问的类型。

typescript
interface StringArray {
  // 索引签名
  [index: number]: string;
}

let arr: StringArray = ['a', 'b']; // 正确
arr[0] = 'c'; // 正确
arr[1] = 123; // 错误,期望是 string 类型
typescript
interface StringDict {
  [key: string]: string;
}

const myDict: StringDict = {
  name: 'Alice',
  age: '30', // 正确,值可以是任意字符串
};

索引签名还可以用作约束,限制对象属性的类型:

typescript
interface ProductDict {
  [key: string]: { name: string; price: number };
}

const products: ProductDict = {
  // 21 : {}  左边写number也可以,因为键名会默认转为字符串
  'p01': { name: 'Banana', price: 1.5 }, // 正确
  'p02': { name: 'Apple', count: 2 }, // 错误,缺少 price 属性
};

应用场景

在重构 JavaScript 代码时,为内部属性较多的对象声明一个 any 的索引签名类型,以此来暂时支持对类型未明确属性的访问,并在后续一点点补全类型:

typescript
interface AnyTypeHere {
  [key: string]: any;
}

const foo: AnyTypeHere['yyds'] = 'any value';
  1. 定义字典类型

典类型通常用于存储键值对,索引签名可以很好地描述这种结构:

typescript
interface StringDict {
  [key: string]: string;
}

const myDict: StringDict = {
  name: 'Alice',
  age: '30',
};
  1. 描述异构数据结构
typescript
interface TupleArray {
  [index: number]: number | string;
}

const arr: TupleArray = [1, 'a', 2, 'b'];
  1. 约束对象属性类型

索引签名还可以用作约束,限制对象属性的类型:

typescript
interface ProductDict {
  [key: string]: { name: string; price: number };
}

const products: ProductDict = {
  'p01': { name: 'Banana', price: 1.5 }, // 正确
  'p02': { name: 'Apple', count: 2 }, // 错误,缺少 price 属性
};
  1. 结合映射类型

索引签名常与映射类型一起使用,为已有类型创建新的类型:

typescript
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };

索引查询类型 keyof T

**keyof T**索引类型查询操作符。 对于任何类型 T,** keyof T**的结果为 T上已知的公共属性名的联合。**keyof T**这种形式会导致一个新的类型。

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

type PersonKey = keyof Person; // "name" | "age"
type PersonAge = Person['age']; // number

keyof Person 返回一个字符串字面量联合类型 "name" | "age"Person['age'] 返回 Person 接口中 age 属性的类型 number

严谨地说,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量

typescript
interface Foo {
  yyds: 1,
  599: 2
}

type FooKeys = keyof Foo; // "yyds" | 599
// 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
// 看不到其中的实际值,你可以这么做:
type FooKeys = keyof Foo & {}; // "yyds" | 599

我们可以写段伪代码来模拟 “从键名到联合类型” 的过程。

typescript
type FooKeys = Object.keys(Foo).join(" | ");

还可以直接 keyof any 来生产一个联合类型,它会由所有可用作对象键值的类型组成:string | number | symbol。也就是说,它是由无数字面量类型组成的,由此我们可以知道, keyof 的产物必定是一个联合类型


应用场景

  1. 常用于泛型

K 的约束类型是 keyof T,这确保了只有 T 的属性名才能作为 key 参数传入。

typescript
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2 };
let a = getProperty(x, 'a'); // 返回值类型为 number
let c = getProperty(x, 'm'); // 错误, 'm' 不是 x 的键
  1. 映射类型

索引查询类型的另一个常见用例是映射类型,比如内置的 Readonly Partial 工具类型:

typescript
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };

**通过 keyof 遍历类型 T 的所有键,并映射出新的类型。**

索引类型查询、索引类型访问通常会和映射类型一起搭配使用,前两者负责访问键,而映射类型在其基础上访问键值类型。

映射类型

映射类型是 TypeScript 中最强大的工具之一,它利用了索引签名的语法,能基于已有的类型创建新的类型。 映射类型指的就是一个确切的类型工具。看到映射这个词应该能联想到 JavaScript 中数组的 map 方法,实际上也是如此,映射类型的主要作用即是基于键名映射到键值类型

内置工具类型如ReadonlyPartialPick 等就是通过映射类型实现的:

typescript
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

eg.

typescript
type Stringify<T> = {
  [K in keyof T]: string;
};

这个工具类型会接受一个对象类型(假设我们只会这么用),使用 keyof 获得这个对象类型的键名组成字面量联合类型,然后通过映射类型(即这里的 in 关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string。

具体表现是这样

typescript
// 将一个接口或对象类型 T 映射为一个新的类型,
// 其中所有属性值的类型都被转换为 string 类型。
type Stringify<T> = {
  [K in keyof T]: string;
};

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 等价于
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}

伪代码如下

typescript

const StringifiedFoo = {};
for (const k of Object.keys(Foo)){
  StringifiedFoo[k] = string;
}

我们应该很少会需要把一个接口的所有属性类型映射到 string?这有什么意义吗?别忘了,既然拿到了键,那键值类型其实也能拿到:

typescript
type Clone<T> = {
  [K in keyof T]: T[K];
};

这里的T[K]其实就是上面说到的索引类型访问,我们使用键的字面量类型访问到了键值的类型,这里就相当于克隆了一个接口。需要注意的是,这里其实只有K in 属于映射类型的语法,keyof T 属于 keyof 索引查询操作符,[K in keyof T]的[]属于索引签名类型,T[K]属于索引类型访问。

in 操作符

在 TypeScript 中,in 操作符有两个主要的用途:

遍历类型

in 操作符可以用于遍历联合类型或者接口/对象类型的键,并基于键创建映射类型。这是 TypeScript 中一个强大的类型操作能力,被称为"映射类型"。

typescript
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

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

type ReadonlyPerson = Readonly<Person>;
// 等同于
// type ReadonlyPerson = {
//     readonly name: string;
//     readonly age: number;
// }

[P in keyof T] 遍历了 T 类型的所有键,并将对应的属性映射为一个新的只读属性。

对象属性检查

in 操作符也可以用于检查某个属性是否存在于对象类型中,返回一个布尔值。

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

const person: Person = { name: 'Alice', age: 30 };

if ('name' in person) {
    // person有name属性
}

if ('gender' in person) {
    // person没有gender属性
}