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) => ...))。