TypeScript 的 Map 就是 JavaScript 内置的 Map,只是键和值带上了类型:Map<string, number> 把字符串映射到数字。用 new Map<K, V>() 创建,然后使用 set、get、has 和 delete。get 返回 V | undefined,因为键可能不存在。
类型参数正是 Map 在 TypeScript 中有用的原因:每次 set 都会被检查,每次 get 都返回值的类型。最后一行是编译错误(TS2345);@ts-expect-error 让这段代码仍然可以运行。
本页讲的是 Map 集合。如果你搜索的是 array.map(),请看最后一节。
创建 Map
类型可以来自类型参数、初始条目或类型标注。初始条目是一个由 [key, value] 元组组成的数组,或者任何能产出这种元组的东西。
两个推断陷阱:
- 不带类型参数也不带条目的
new Map()是Map<any, any>,放进去和取出来的东西都不会被检查。请始终写成new Map<K, V>()。 - 值类型不同的条目不会被推断成联合类型。
new Map([["a", 1], ["b", "x"]])会报错 TS2769(No overload matches this call)。请写出类型:new Map<string, number | string>([...])。
get 返回 V | undefined
Map 无法保证某个键一定存在,所以 get 的类型是 V | undefined。在 strict 模式下,把值当作 V 使用之前,你必须先处理 undefined:
index.ts(4,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.
修复方法,按常用程度排列:
之后调用 get 时,TypeScript 并不记得 has 返回过 true。直接检查 get 的结果既更简短又安全。非空断言 stock.get("apples")! 能让错误消失,但键不存在时它不提供任何保护。
Map 的方法及其类型
| 成员 | 在 Map<K, V> 上的类型 | 说明 |
|---|---|---|
new Map<K, V>(entries?) | Map<K, V> | entries:由 [K, V] 组成的可迭代对象 |
set(key, value) | this | 添加或替换;可链式调用 |
get(key) | V | undefined | 键不存在时为 undefined |
has(key) | boolean | |
delete(key) | boolean | 删除了内容时为 true |
clear() | void | 清空所有条目 |
size | number | 是属性,不是方法 |
keys()、values() | K、V 的迭代器 | 展开成数组:[...map.keys()] |
entries()、for...of | [K, V] 的迭代器 | 按插入顺序 |
forEach((value, key) => ...) | void | 注意:值在前 |
遍历 Map
Map 按插入顺序遍历。对 Map 使用 for...of 会产出 [key, value] 元组,类型为 [K, V]。
给已有的键赋值会更新它的值,但保留它原来在顺序中的位置。
以对象为键与计数
任何值都可以作键,包括对象和数组。键的比较方式和 === 一样:内容相同的两个对象是不同的键。Map 也是计数或分组的标准做法。
如果要按对象的内容作键,就改用从中派生的字符串或数字键,比如 user.id 或 `${x},${y}`。
Map、对象与 Record 的对比
Map<K, V> | 对象 / Record<string, V> | |
|---|---|---|
| 键的类型 | 任意类型,按 === 比较 | string(数字会变成字符串)、symbol |
| 缺失键的类型 | get 返回 V | undefined | obj[key] 是 V,除非开启了 noUncheckedIndexedAccess |
| 顺序 | 插入顺序 | 基本是插入顺序,但类似整数的键会按升序排在最前 |
| 大小 | map.size | Object.keys(obj).length |
| 频繁增删 | 为此做了优化 | 没有为此优化 |
| JSON | 不能直接序列化(JSON.stringify(map) 是 "{}") | 直接序列化 |
| 字面量语法、解构 | 不支持 | 支持 |
| 意外继承来的键 | 没有 | "toString" in {} 为 true |
经验法则:键本身是数据(用户 id、单词、缓存条目)并且会在运行时变化的集合,用 Map;键是固定已知的一组,或者数据要和 JSON 互相转换时,用对象类型或 Record。字典页面对比了用于字符串键查找的索引签名、Record 和 Map。
Map 与对象和 JSON 的互相转换
Map 的条目不是属性,所以 JSON.stringify 看不到它们。请借助 Object.fromEntries 和 Object.entries 转换:
JSON.parse 返回 any,所以用 as Record<...> 说明数据应当是什么样子。它不是运行时检查;在信任这个类型之前,请先校验不可信的 JSON。
array.map() 的类型
很多人搜索“typescript map”时指的是数组方法,它会转换每个元素并返回一个新数组。它的类型由回调推断,所以很少需要标注:
标注回调的返回类型((u): Option => ...)是说明结果类型最清晰的方式:这样返回的对象中缺少属性或者属性名拼错,都会在回调处产生编译错误。
常见问题
TypeScript 中如何创建 Map?
把键和值的类型传给构造函数:const ages = new Map<string, number>()。带初始条目时类型会被推断出来:new Map([["ada", 36]]) 是 Map<string, number>。不带类型也不带条目的 new Map() 是 Map<any, any>,相当于关闭了类型检查,所以一定要写上类型。
为什么 TypeScript 中 Map.get 会返回 undefined?
map.get(key) 的类型是 V | undefined,因为这个键可能不存在。TypeScript 不会把之前的 map.has(key) 和之后的 get 联系起来,所以即使调用过 has,你也要处理 undefined:把结果存下来再检查,或者用 ?? 提供默认值。
TypeScript 中 Map 和对象有什么区别?
Map 接受任意类型的键(包括对象),保持插入顺序,有 size 属性,并且为频繁的增删而设计。普通对象或 Record<string, V> 只有字符串(和 symbol)键,可以直接序列化为 JSON,支持字面量语法和解构。动态的键值集合用 Map,固定结构和 JSON 数据用对象。
TypeScript 中如何把 Map 转成对象或 JSON?
Object.fromEntries(map) 把 Map<string, V> 转成普通对象,然后就可以用 JSON.stringify 序列化。直接对 Map 调用 JSON.stringify(map) 得到的是 "{}",因为 Map 的条目不是属性。反过来则是 new Map(Object.entries(obj))。
TypeScript 中如何给 array.map 的回调加类型?
通常不需要:items.map((item) => item.name) 会根据数组推断出 item,并根据回调的返回值推断结果类型。如果要强制指定结果类型,就把它作为类型参数传入,items.map<string>(...),或者标注回调的返回类型。