TypeScript có các vòng lặp (loop) giống JavaScript: for, for...of, for...in, while, do...while, và phương thức mảng forEach. Thứ TypeScript thêm vào là kiểu: biến lặp của for...of mang kiểu phần tử, còn khóa của for...in luôn là string.
Với hầu hết các vòng lặp qua mảng, for...of là lựa chọn nên dùng: dễ đọc, hỗ trợ break, continue và await, và không cần chú thích kiểu.
Vòng lặp for cổ điển
for (init; condition; update) là vòng lặp để đếm, bước nhảy lớn hơn một, hoặc đi ngược. TypeScript suy ra kiểu của biến đếm từ giá trị ban đầu.
Khai báo biến đếm bằng let, không dùng var. Với let, mỗi lần lặp có binding riêng, nên một callback tạo ra trong vòng lặp sẽ thấy giá trị của đúng lần lặp đó.
for...of: giá trị của mảng, chuỗi và Map
for...of hoạt động trên mọi thứ iterable, và biến lặp mang kiểu tương ứng. Để lấy cả chỉ số, hãy lặp qua array.entries(), hàm này trả về các tuple [index, value].
break thoát khỏi vòng lặp còn continue nhảy sang phần tử tiếp theo, đúng như trong vòng lặp for. Chi tiết lúc chạy (iterable, generator) nằm ở trang JavaScript for...of và for...in.
forEach: không break, không await
forEach gọi một hàm cho mỗi phần tử và trả về undefined. Tham số của callback lấy kiểu từ mảng, nên không cần chú thích. Có hai việc nó không làm được:
- Dừng sớm.
breakbên trong callback là lỗi biên dịch TS1107,Jump target cannot cross function boundary.Cònreturnchỉ kết thúc lần gọi hiện tại, nên nó hoạt động nhưcontinue. - Chờ công việc bất đồng bộ.
forEachbỏ qua promise mà một callbackasynctrả về.
Cái bẫy async rất dễ bị bỏ sót, vì code biên dịch mà không có cảnh báo nào:
Các callback của forEach kết thúc theo thứ tự hẹn giờ (10, 20, 30) sau khi vòng lặp đã đi tiếp. Phiên bản for...of chạy chúng tuần tự. Với công việc song song mà bạn vẫn muốn chờ, hãy dùng await Promise.all(items.map(async (item) => { ... })).
for...in: khóa là chuỗi
for...in lặp qua các tên thuộc tính enumerable của một đối tượng. Kiểu của khóa luôn là string, kể cả với mảng, nơi các khóa là chỉ số dưới dạng chuỗi. Điều đó gây khó khi truy cập một đối tượng có kiểu:
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 cố ý khai báo khóa là string: lúc chạy, một đối tượng có thể có nhiều thuộc tính hơn kiểu của nó liệt kê (structural typing cho phép thuộc tính thừa), và for...in còn duyệt cả các thuộc tính enumerable được kế thừa. Vì vậy nó không thể hứa khóa là "name" | "age". Cách sửa có ở phần tiếp theo. Đừng dùng for...in cho mảng.
Lặp qua khóa và giá trị của đối tượng
Object.entries trả về các cặp [key, value] và là cách thông dụng để lặp qua một đối tượng. Object.keys trả về string[] với cùng lý do như for...in, nên khi bạn biết đối tượng không có khóa thừa, hãy ép kiểu cho khóa.
Với dữ liệu mà khóa không biết trước, hãy khai báo đối tượng là Record<string, number> hoặc dùng Map; cả hai được trình bày ở trang dictionary.
while và do...while
while kiểm tra điều kiện trước mỗi lượt; do...while chạy thân vòng lặp một lần rồi mới kiểm tra. Chúng phù hợp với những vòng lặp không duyệt qua một tập hợp, chẳng hạn đọc cho đến khi một điều kiện thỏa mãn. Việc thu hẹp kiểu vẫn hoạt động bên trong: sau phép kiểm tra !== undefined ở điều kiện, biến được thu hẹp kiểu trong thân vòng lặp.
Nhãn, break và continue trong vòng lặp lồng nhau
break thông thường chỉ thoát vòng lặp trong cùng. Để thoát vòng lặp bên ngoài, hãy gắn nhãn cho nó và gọi tên nhãn:
Nên dùng vòng lặp nào
| Mục đích | Vòng lặp |
|---|---|
| Mọi phần tử của mảng | for (const x of arr) |
| Phần tử và chỉ số | for (const [i, x] of arr.entries()) hoặc arr.forEach((x, i) => ...) |
| Dừng sớm | for...of với break, hoặc find, some, every |
| Chờ từng bước | for...of với await bên trong |
| Tạo mảng mới | map, filter (không phải vòng lặp) |
| Đếm hoặc bước nhảy n | for (let i = 0; ...; i += n) |
| Khóa và giá trị của đối tượng | for (const [k, v] of Object.entries(obj)) |
| Các mục của Map | for (const [k, v] of map) |
| Cho đến khi thỏa điều kiện | while |
Câu hỏi thường gặp
Viết vòng lặp for trong TypeScript như thế nào?
Giống hệt JavaScript: for (let i = 0; i < 5; i++) { ... }; TypeScript suy ra i có kiểu number. Để duyệt các phần tử của mảng, for (const item of items) ngắn gọn hơn, và item mang kiểu phần tử của mảng.
Làm sao để thoát khỏi forEach trong TypeScript?
Không làm được: break bên trong callback là lỗi biên dịch TS1107 (Jump target cannot cross function boundary), còn return chỉ kết thúc lần gọi callback hiện tại, giống continue. Hãy dùng for...of với break, hoặc some/find/every, vốn được thiết kế để dừng sớm.
for...of và for...in trong TypeScript khác nhau thế nào?
for...of duyệt các giá trị của một iterable (phần tử mảng, ký tự của chuỗi, các mục của Map) với đúng kiểu của chúng. for...in duyệt các khóa thuộc tính enumerable của một đối tượng, luôn có kiểu string, kể cả chỉ số của mảng dưới dạng chuỗi. Hãy dùng for...of cho mảng và Object.entries cho đối tượng.
Làm sao để lặp qua khóa và giá trị của một đối tượng trong TypeScript?
Dùng for (const [key, value] of Object.entries(obj)). key là string và value là union các kiểu thuộc tính. Nếu bạn cần key có kiểu keyof typeof obj, hãy ép kiểu: (Object.keys(obj) as (keyof typeof obj)[]), vì TypeScript cố ý khai báo Object.keys trả về string[].
Có dùng await bên trong forEach được không?
Code vẫn biên dịch được, nhưng forEach không chờ các promise mà callback trả về, nên vòng lặp kết thúc trước khi công việc xong. Hãy dùng for...of với await bên trong để chạy tuần tự, hoặc await Promise.all(items.map(async (x) => ...)) để chạy song song.