12k
All articles

TypeScript 字典:类型安全对象的完整指南

对比索引签名、Record 类型与 Map 三种 TypeScript 字典实现方式,构建类型安全的键值结构,有效减少运行时错误。

OpenReplay Team
OpenReplay Team
TypeScript 字典:类型安全对象的完整指南

在 TypeScript 中处理键值对时,你需要一种可靠的方式来在访问和操作数据时保持类型安全。与具有内置字典类型的语言不同,TypeScript 需要特定的方法来创建类型安全的字典。本指南探讨了所有实现选项,并提供实用示例,帮助你为项目选择正确的方法。

要点

  • TypeScript 提供多种实现字典的方式:索引签名、Record 工具类型和 Map
  • 根据需求选择实现方式:简单字典用索引签名,特定键集用 Record,高级功能用 Map
  • 利用 TypeScript 的类型系统确保字典操作的编译时安全
  • 在选择基于对象的字典和 Map 时考虑性能影响
  • 对于复杂字典需求,使用映射类型等高级类型技术

什么是 TypeScript 字典?

TypeScript 中的字典是一种存储键值对的数据结构,允许通过键进行高效查找。虽然 TypeScript 没有原生的 Dictionary 类型,但它提供了几种方式来实现具有完全类型安全的字典结构。

类型安全的字典确保:

  • 键和值符合特定类型
  • 类型不匹配时产生编译时错误
  • 支持自动完成和 IntelliSense
  • 防止运行时错误

TypeScript 中的字典实现选项

使用索引签名

最直接的方法是使用带有 TypeScript 索引签名的 JavaScript 对象:

// 基本索引签名字典
const userScores: { [username: string]: number } = {};

// 添加条目
userScores[""alice""] = 95;
userScores[""bob""] = 87;

// 访问值
console.log(userScores[""alice""]); // 95

// 类型安全实践
userScores[""charlie""] = ""high""; // 错误:类型 'string' 不能赋值给类型 'number'

这种方法很简单,但在代码库中重复使用时会变得繁琐。

创建可重用的字典类型

为了更好的可重用性,定义一个泛型字典类型:

// 泛型字典类型
type Dictionary<K extends string | number | symbol, V> = {
  [key in K]: V;
};

// 使用字符串键和数字值
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;

// 使用数字键
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";

使用 Record 工具类型

TypeScript 内置的 Record 工具类型提供了更简洁的语法:

// 使用 Record 表示固定键集
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
  name: ""Alice Smith"",
  email: ""alice@example.com"",
  role: ""Admin""
};

// 使用 Record 表示任意字符串键
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;

当你需要使用字符串字面量类型强制执行特定的键集时,Record 类型特别有用。

在 TypeScript 中使用 JavaScript 的 Map

对于更高级的功能,使用 JavaScript 的 Map 对象配合 TypeScript 泛型:

// 类型安全的 Map
const userProfiles = new Map<string, {age: number, active: boolean}>();

// 添加条目
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});

// 类型检查有效
userProfiles.set(""charlie"", {age: ""thirty""}); // 错误:类型 'string' 不能赋值给类型 'number'

// 访问值
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28

字典实现比较

特性 带索引签名的对象 Record 类型 Map 键类型 字符串、数字、符号 任何类型 任何类型 性能 小数据集性能好 小数据集性能好 频繁增删性能更好 内存使用 较低 较低 较高 顺序保留 否 否 是(插入顺序) 迭代 需要 Object.entries() 需要 Object.entries() 内置迭代器 特殊方法 无 无 has()delete()clear() 原型污染风险 是 是 否

常见字典操作

检查键是否存在

// 使用索引签名
const hasKey = (dict: Record<string, unknown>, key: string): boolean => {
  return key in dict;
};

// 使用 Map
const userMap = new Map<string, number>();
userMap.has(""alice""); // true/false

添加和更新值

// 使用索引签名
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};

// 添加
users[""u1""] = {name: ""Alice"", role: ""Admin""};

// 更新
users[""u1""] = {...users[""u1""], role: ""User""};

// 使用 Map
const userMap = new Map<string, {name: string, role: string}>();
userMap.set(""u1"", {name: ""Alice"", role: ""Admin""});
userMap.set(""u1"", {name: ""Alice"", role: ""User""}); // 更新现有条目

删除键

// 使用索引签名
delete users[""u1""];

// 使用 Map
userMap.delete(""u1"");

遍历字典

// 基于对象的字典遍历
const scores: Record<string, number> = {math: 95, science: 87};

// 方法 1:Object.entries()
for (const [subject, score] of Object.entries(scores)) {
  console.log(`${subject}: ${score}`);
}

// 方法 2:Object.keys()
Object.keys(scores).forEach(subject => {
  console.log(`${subject}: ${scores[subject]}`);
});

// Map 遍历
const scoreMap = new Map<string, number>([
  [""math"", 95],
  [""science"", 87]
]);

// 直接遍历
for (const [subject, score] of scoreMap) {
  console.log(`${subject}: ${score}`);
}

// 使用 forEach
scoreMap.forEach((score, subject) => {
  console.log(`${subject}: ${score}`);
});

高级类型安全技术

约束键类型

// 使用字符串字面量联合类型
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
  id: ""123"",
  name: ""Alice"",
  email: ""alice@example.com""
};

// 尝试使用无效键
userData.phone = ""555-1234""; // 错误:属性 'phone' 不存在

只读字典

// 不可变字典
const constants: Readonly<Record<string, number>> = {
  MAX_USERS: 100,
  TIMEOUT_MS: 5000
};

constants.MAX_USERS = 200; // 错误:无法分配到 'MAX_USERS' ,因为它是只读属性

部分字典

// 带有可选值的字典
type UserProfile = {
  name: string;
  email: string;
  age: number;
};

// 创建一个字典,其中每个值可以有部分或全部 UserProfile 属性
const profiles: Record<string, Partial<UserProfile>> = {};

profiles[""alice""] = { name: ""Alice"" }; // 有效,即使没有 email 和 age
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // 有效

使用映射类型创建高级字典

type SensorData = {
  temperature: number;
  humidity: number;
  pressure: string;
  metadata: object;
};

// 创建一个字典类型,只包含 SensorData 中的数字属性
type NumericSensorDict = {
  [K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};

// 结果类型只有 temperature 和 humidity 作为有效键
const sensorStatus: NumericSensorDict = {
  temperature: true,
  humidity: false
};

性能考虑

选择字典实现时,考虑以下因素:

  1. 数据大小:对于带有字符串键的小型数据集,基于对象的字典效率高
  2. 频繁修改:对于频繁的添加和删除操作,Map 表现更好
  3. 内存使用:基于对象的字典比 Map 使用更少的内存
  4. 键类型:如果需要非字符串键,使用 Map

10,000 次操作的基准测试结果:

对象访问:~25ms
Map 访问:~45ms
对象插入:~30ms
Map 插入:~50ms
对象删除:~20ms
Map 删除:~15ms

实际应用场景

配置管理

type AppConfig = Record<string, string | number | boolean>;

const config: AppConfig = {
  apiUrl: ""https://api.example.com"",
  timeout: 5000,
  enableCache: true
};

缓存实现

class Cache<T> {
  private store = new Map<string, {value: T, expiry: number}>();
  
  set(key: string, value: T, ttlSeconds: number): void {
    const expiry = Date.now() + (ttlSeconds * 1000);
    this.store.set(key, {value, expiry});
  }
  
  get(key: string): T | null {
    const item = this.store.get(key);
    
    if (!item) return null;
    if (item.expiry < Date.now()) {
      this.store.delete(key);
      return null;
    }
    
    return item.value;
  }
}

const userCache = new Cache<{name: string, role: string}>();
userCache.set(""user:123"", {name: ""Alice"", role: ""Admin""}, 300);

React 中的状态管理

interface UserState {
  users: Record<string, {name: string, role: string}>;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  users: {},
  loading: false,
  error: null
};

// 在 reducer 中
function userReducer(state = initialState, action: any): UserState {
  switch (action.type) {
    case 'ADD_USER':
      return {
        ...state,
        users: {
          ...state.users,
          [action.payload.id]: {
            name: action.payload.name,
            role: action.payload.role
          }
        }
      };
    // 其他情况...
    default:
      return state;
  }
}

错误处理策略

处理缺失的键

function safeGet<K extends string, V>(
  dict: Record<K, V>, 
  key: string, 
  defaultValue: V
): V {
  return (key in dict) ? dict[key as K] : defaultValue;
}

const users: Record<string, string> = {
  ""1"": ""Alice""
};

const userName = safeGet(users, ""2"", ""Unknown User"");
console.log(userName); // ""Unknown User""

字典值的类型守卫

function isUserDict(obj: unknown): obj is Record<string, {name: string, age: number}> {
  if (typeof obj !== 'object' || obj === null) return false;
  
  for (const key in obj as object) {
    const value = (obj as any)[key];
    if (typeof value !== 'object' || 
        typeof value.name !== 'string' || 
        typeof value.age !== 'number') {
      return false;
    }
  }
  
  return true;
}

// 使用示例
function processUserData(data: unknown) {
  if (isUserDict(data)) {
    // TypeScript 知道 data 是 Record<string, {name: string, age: number}>
    for (const [id, user] of Object.entries(data)) {
      console.log(`User ${id}: ${user.name}, ${user.age}`);
    }
  }
}

结论

TypeScript 字典提供了强大的方式来管理具有完全类型安全的键值数据。通过根据特定需求选择正确的实现方法,你可以构建更健壮的应用程序,减少运行时错误。无论你需要简单的字符串键对象还是具有高级功能的复杂字典,TypeScript 的类型系统都能确保你的字典操作在整个代码库中保持类型安全。

常见问题

什么时候应该使用 Map 而不是基于对象的字典?

当你需要非字符串键、保证键顺序、频繁添加和删除操作,或防止原型污染时,应使用 Map。Map 提供了内置方法如 has()、delete() 和 clear(),使某些操作更加便捷。

如何创建具有固定键集的字典?

使用字符串字面量类型和 Record 工具类型:nn`typescriptntype AllowedKeys = 'id' | 'name' | 'email';nconst user: Record<AllowedKeys, string> = {n id: '123',n name: 'Alice',n email: 'alice@example.com'n};n`nn这确保如果你尝试使用不在 AllowedKeys 联合类型中的键,TypeScript 会报错。

我可以在 TypeScript 字典中使用复杂对象作为键吗?

不能在基于对象的字典中使用,但可以在 Map 中使用:nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // 不同的对象引用nnuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined (不同的对象引用)n`nn注意,Map 对对象键使用引用相等性。

如何在对象字典和 Map 之间转换?

将对象转换为 Map:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nn将 Map 转换为对象:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`

如何确保访问字典值时的空值安全?

使用可选链和空值合并运算符:nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// 安全访问并提供备选值nconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nn这可以防止在访问可能为 undefined 的值的属性时出现运行时错误。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.