Back

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

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 提供了内置方法如 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 会报错。

不能在基于对象的字典中使用,但可以在 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: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