Menu

TypeScript Map 用法:Map<K, V>、get、set、has 与遍历

TypeScript 中 Map 的用法:创建带类型的 Map<K, V>,为什么 get 返回 V | undefined,set、has 和 delete,按插入顺序遍历,以对象作键,与对象和 JSON 互相转换,Map、对象与 Record 的对比,以及 array.map() 的类型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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清空所有条目
sizenumber是属性,不是方法
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 | undefinedobj[key] 是 V,除非开启了 noUncheckedIndexedAccess
顺序插入顺序基本是插入顺序,但类似整数的键会按升序排在最前
大小map.sizeObject.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>(...),或者标注回调的返回类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始