为什么这个话题在 Stack Overflow 上如此火爆?
\n\n如果你曾经用 TypeScript 编写过代码,并试图创建一个所有值都必须是同一类型的对象,那么你很可能遇到过这个问题。问题 "Enforcing the type of the indexed members of a Typescript object" 在 Stack Overflow 上获得了数千次浏览并非偶然——这是从 JavaScript 转来的开发人员常犯的经典错误。
\n\n想象一下:你有一个缓存字典、一个带有动态键的配置,或者只是一个包含数字的映射。你希望 TypeScript 确保对象中的每个值都是字符串、数字或某种复杂类型。但编译器却莫名其妙地忽略了错误。熟悉吗?
\n\n在本文中,我们将介绍解决此任务的所有主要方法:从简单的 Index Signatures 到高级的 Mapped Types。你将了解何时使用哪种方法以及会遇到哪些陷阱。
问题:具有动态键的对象
\n\n假设我们正在编写一个用户管理系统。我们有一个对象,其键是用户 ID,值是他们的名字:
\n\ninterface UserCache {\n [userId: string]: string;\n}\n\nconst cache: UserCache = {\n 'user_1': 'Alice',\n 'user_2': 'Bob',\n // 'user_3': 123; // 错误!必须是字符串\n};\n\ncache['user_4'] = 'Charlie'; // 正确\ncache['user_5'] = 42; // 错误!类型 'number' 不能赋值给类型 'string'\n\n\n看起来一切正常。但有一个问题:Index Signature 允许任何字符串键。如果我们想限制允许的键集呢?
方法 1:Index Signature——经典方法
\n\nIndex Signature 是为对象的所有值设置类型的最简单方法。语法:
\n\ninterface StringMap {\n [key: string]: string;\n}\n\nconst map: StringMap = {\n hello: 'world',\n foo: 'bar',\n};\n\n\n优点:
\n- \n
- 简单易读 \n
- 适用于任意数量的键 \n
缺点:
\n- \n
- 无法限制键集 \n
- 所有值必须是同一类型(或联合类型) \n
- 不适用于
undefined值(需要小心) \n
新手常犯的错误:尝试向此类对象添加方法:
\n\ninterface BadMap {\n [key: string]: string;\n length: number; // 错误!'number' 不能赋值给 'string'\n}\n\n\n这行不通,因为 length 违反了通用类型。如果需要混合类型,请使用联合类型或映射类型。
方法 2:Record——内置辅助类型
\n\nTypeScript 提供了内置类型 Record<K, V>,它做同样的事情,但语法更简洁:
type StringRecord = Record<string, string>;\n\nconst config: StringRecord = {\n apiUrl: 'https://api.example.com',\n timeout: '5000', // 正确,字符串\n // port: 8080, // 错误!\n};\n\n// 可以限制键:\ntype AllowedKeys = 'name' | 'email' | 'phone';\ntype UserRecord = Record<AllowedKeys, string>;\n\nconst user: UserRecord = {\n name: 'Alice',\n email: 'alice@example.com',\n phone: '+1234567890',\n // age: '30', // 错误!'age' 不在 AllowedKeys 中\n};\n\n\n何时使用:
\n- \n
- 需要快速创建类型化字典时 \n
- 键是特定的值集(联合类型)时 \n
- 用于具有已知字段的配置 \n
缺点: Record 不允许为不同的键设置不同的类型。为此,有映射类型。
方法 3:Map——来自 ES6 的替代方案
\n\n如果你使用 JavaScript 的 Map,TypeScript 可以很好地对其进行类型化