Menu

TypeScript 循环:for、forEach、for...of 与 for...in

TypeScript 中的每一种循环写法,以及各自带来的类型:经典 for 循环,遍历数组、Map 和字符串的 for...of,键总是字符串的 for...in,forEach(不能 break,也不能 await),按类型遍历对象的键,以及 while。

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

TypeScript 的循环和 JavaScript 完全相同:for、for...of、for...in、while、do...while,以及数组方法 forEach。TypeScript 增加的是类型:for...of 的循环变量带有元素类型,而 for...in 的键永远是 string。

大多数遍历数组的场景都应该首选 for...of:它读起来清晰,支持 break、continue 和 await,而且不需要任何类型标注。

经典 for 循环

for (init; condition; update) 适合计数、每次跨多步或者倒序遍历。TypeScript 会根据初始值推断计数器的类型。

用 let 而不是 var 声明计数器。使用 let 时每次迭代都有自己的绑定,所以在循环中创建的回调看到的是那一次迭代的值。

for...of:遍历数组、字符串和 Map 的值

for...of 适用于任何可迭代对象,循环变量会得到对应的类型。如果还需要索引,就遍历 array.entries(),它会产出 [index, value] 元组。

break 退出循环,continue 跳到下一项,和 for 循环中完全一样。运行时的细节(可迭代对象、生成器)见 JavaScript 的 for...of 与 for...in 页面。

forEach:不能 break,也不能 await

forEach 为每个元素调用一次函数,返回 undefined。回调的参数类型来自数组,所以不需要标注。它有两件事做不到:

  • 提前停止。 在回调里写 break 是编译错误 TS1107,Jump target cannot cross function boundary.。return 只结束当前这次调用,所以效果和 continue 一样。
  • 等待异步操作。 forEach 会忽略 async 回调返回的 promise。

异步的陷阱很容易被忽略,因为它编译时没有任何警告:

forEach 的回调按定时器顺序(10、20、30)完成,而此时循环早已继续往下执行。for...of 版本则按顺序逐个执行。如果想并行执行又要等待全部完成,用 await Promise.all(items.map(async (item) => { ... }))。

for...in:键是字符串

for...in 遍历对象的可枚举属性名。键的类型永远是 string,数组也不例外,它的键就是字符串形式的索引。这让它在索引带类型的对象时很别扭:

index.ts(5,22): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'.
  No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.

TypeScript 有意把键的类型定为 string:运行时一个对象的属性可能比它的类型列出的更多(结构类型允许多余的属性),而且 for...in 还会遍历继承来的可枚举属性。因此它无法保证键一定是 "name" | "age"。解决方法见下一节。数组则干脆不要用 for...in。

遍历对象的键和值

Object.entries 返回 [key, value] 键值对,是遍历对象的常用方式。Object.keys 返回 string[],原因和 for...in 相同,所以当你确定对象没有多余的键时,就对键的类型做断言。

对于事先不知道有哪些键的数据,把对象类型定为 Record<string, number> 或者使用 Map;两者在字典页面都有介绍。

while 与 do...while

while 在每轮之前检查条件;do...while 先执行一次循环体再检查。它们适合不是遍历集合的循环,比如一直读取直到某个条件成立。类型收窄在其中同样有效:条件里做了 !== undefined 检查之后,循环体中的变量就会被收窄。

嵌套循环中的标签、break 和 continue

普通的 break 只会退出最内层的循环。要退出外层循环,就给它加上标签,并在 break 后写出标签名:

该用哪种循环

目标循环
数组的每个元素for (const x of arr)
元素和索引for (const [i, x] of arr.entries()) 或 arr.forEach((x, i) => ...)
提前停止带 break 的 for...of,或 find、some、every
每一步都 await在内部 await 的 for...of
构建新数组map、filter(不是循环)
计数或每次跨 n 步for (let i = 0; ...; i += n)
对象的键和值for (const [k, v] of Object.entries(obj))
Map 的条目for (const [k, v] of map)
直到某个条件成立while

常见问题

TypeScript 中怎么写 for 循环?

和 JavaScript 一样:for (let i = 0; i < 5; i++) { ... },TypeScript 会把 i 推断为 number。要遍历数组元素,for (const item of items) 更简短,item 会得到数组的元素类型。

TypeScript 中怎么跳出 forEach?

做不到:在回调里写 break 是编译错误 TS1107(Jump target cannot cross function boundary),而 return 只会结束当前这次回调,效果相当于 continue。请改用带 break 的 for...of,或者 some/find/every,它们本来就会提前停止。

TypeScript 中 for...of 和 for...in 有什么区别?

for...of 遍历可迭代对象的值(数组元素、字符串的字符、Map 的条目),并带有它们真实的类型。for...in 遍历对象的可枚举属性键,类型总是 string,数组的索引也会以字符串形式出现。数组用 for...of,对象用 Object.entries。

TypeScript 中如何遍历对象的键和值?

使用 for (const [key, value] of Object.entries(obj))。key 是 string,value 是各属性类型的联合。如果需要把 key 类型定为 keyof typeof obj,就用断言:(Object.keys(obj) as (keyof typeof obj)[]),因为 TypeScript 有意把 Object.keys 的类型定为 string[]。

forEach 里可以用 await 吗?

可以编译通过,但 forEach 不会等待回调返回的 promise,所以循环在工作完成之前就结束了。需要按顺序执行时,用在内部 await 的 for...of;需要并行执行时,用 await Promise.all(items.map(async (x) => ...))。

Coddy 编程语言插图

和 Coddy 一起学编程

开始