TypeScript 索引签名详解

目录

  1. 概述
  2. 基本语法
  3. 常见用法
  4. 高级特性
  5. 最佳实践
  6. 常见陷阱

概述

索引签名允许我们定义对象可以包含的属性的类型模式,而不需要明确列出所有属性。

基本语法

字符串索引签名

interface StringIndex {
  [key: string]: string;  // 基本字符串索引签名
}

const names: StringIndex = {
  first: "John",
  last: "Doe",
  middle: "Smith"
};

数字索引签名

interface NumberIndex {
  [index: number]: string;  // 基本数字索引签名
}

const arr: NumberIndex = {
  0: "First",
  1: "Second",
  2: "Third"
};

混合索引签名

interface MixedIndex {
  [key: string]: any;
  length: number;    // 具体属性
  name: string;      // 具体属性
}

常见用法

1. 动态属性对象

interface Dictionary<T> {
  [key: string]: T;
}

// 使用示例
const stringDict: Dictionary<string> = {
  key1: "value1",
  key2: "value2"
};

const numberDict: Dictionary<number> = {
  count: 10,
  age: 20
};

2. 类数组对象

interface ArrayLike {
  [index: number]: string;
  length: number;
}

const list: ArrayLike = {
  0: "first",
  1: "second",
  length: 2
};

3. 映射类型

interface PersonMap {
  [id: string]: {
    name: string;
    age: number;
  }
}

const people: PersonMap = {
  "123": { name: "John", age: 30 },
  "456": { name: "Jane", age: 25 }
};

高级特性

1. 只读索引签名

interface ReadOnlyIndex {
  readonly [key: string]: string;
}

const config: ReadOnlyIndex = {
  api: "http://api.example.com",
  token: "abc123"
};

// 错误:索引签名是只读的
config.api = "new-url";  // Error

2. 联合类型索引签名

interface FlexibleIndex {
  [key: string]: string | number;
  count: number;
  name: string;
}

const obj: FlexibleIndex = {
  count: 1,
  name: "test",
  extra: "additional",
  value: 42
};

3. 条件类型与索引签名

type DynamicIndex<T> = {
  [K in keyof T]: T[K] extends string ? string : number;
}

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

type PersonIndex = DynamicIndex<Person>;
// 结果:{ name: string; age: number; }

最佳实践

1. 使用泛型增加灵活性

interface GenericDict<T> {
  [key: string]: T;
}

function createDict<T>(items: T[]): GenericDict<T> {
  const dict: GenericDict<T> = {};
  items.forEach((item, index) => {
    dict[`item${index}`] = item;
  });
  return dict;
}

2. 类型安全的键值对

interface TypeSafeDict<K extends string | number | symbol, V> {
  [key: K]: V;
}

// 使用字符串字面量类型
type ValidKeys = 'id' | 'name' | 'email';
interface UserDict {
  [key in ValidKeys]: string;
}

3. 混合具体属性和索引签名

interface MixedProps {
  id: number;                 // 具体属性
  name: string;              // 具体属性
  [key: string]: any;        // 额外的动态属性
}

const user: MixedProps = {
  id: 1,
  name: "John",
  extraProp: true,
  anotherProp: 42
};

常见陷阱

1. 数字索引与字符串索引的关系

interface NumberAndString {
  [index: number]: string;
  [key: string]: string | undefined;  // 必须包含数字索引的返回类型
}

// 错误示例
interface Wrong {
  [index: number]: string;
  [key: string]: number;  // Error: 数字索引类型必须是字符串索引类型的子类型
}

2. 属性检查

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

const dict: StringDict = {
  prop: "value"
};

// 运行时可以,但不推荐
const value = dict["nonexistent"];  // undefined

// 更安全的访问方式
const value2 = "prop" in dict ? dict["prop"] : undefined;

3. 索引签名与可选属性

interface OptionalProps {
  [key: string]: string | undefined;
  required: string;          // OK
  optional?: string;         // OK
}

// 错误示例
interface WrongOptional {
  [key: string]: string;
  optional?: string;         // Error: 可选属性必须匹配索引签名
}

实际应用示例

1. 缓存实现

interface Cache<T> {
  [key: string]: {
    value: T;
    timestamp: number;
  }
}

class DataCache<T> {
  private cache: Cache<T> = {};

  set(key: string, value: T): void {
    this.cache[key] = {
      value,
      timestamp: Date.now()
    };
  }

  get(key: string): T | undefined {
    return this.cache[key]?.value;
  }
}

2. 表单数据处理

interface FormData {
  [fieldName: string]: string | string[] | undefined;
}

function processForm(data: FormData): void {
  Object.entries(data).forEach(([field, value]) => {
    if (Array.isArray(value)) {
      console.log(`${field}: Multiple values`, value);
    } else if (value) {
      console.log(`${field}: Single value`, value);
    }
  });
}

3. API 响应处理

interface ApiResponse<T> {
  data: T;
  meta: {
    [key: string]: unknown;
  };
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

总结

  1. 索引签名的主要用途:
  • 定义动态属性对象
  • 实现字典或映射
  • 处理未知属性集
  1. 最佳实践:
  • 使用泛型增加类型安全
  • 合理混合具体属性和索引签名
  • 注意数字索引和字符串索引的关系
  1. 注意事项:
  • 属性类型必须匹配索引签名
  • 考虑使用 Map 或 Set 替代索引签名
  • 注意可选属性与索引签名的兼容性