Vấn đềLỗi này thường xảy ra ngay khi bạn đang xây dựng thứ gì đó tham vọng—như bộ làm phẳng đối tượng sâu (object flattener), trình tạo truy vấn ORM tùy chỉnh hoặc trình ánh xạ trạng thái lồng nhau. Đột nhiên, trình biên dịch TypeScript "bỏ cuộc". Bạn sẽ thấy lỗi này nhấp nháy trong IDE hoặc làm dừng tiến trình CI/CD của mình:
Type instantiation is excessively deep and possibly infinite.
Trình biên dịch TypeScript sử dụng một giới hạn đệ quy tích hợp để duy trì hiệu suất. Thông thường, giới hạn này nằm ở mức khoảng 50 cấp độ sâu. Nếu một kiểu generic tự gọi chính nó quá nhiều lần mà không có điều kiện thoát rõ ràng, trình biên dịch sẽ dừng tiến trình. Điều này giúp ngăn CPU của bạn hoạt động 100% và làm treo toàn bộ môi trường phát triển.
Tại sao điều này xảy raTrình biên dịch kích hoạt cơ chế ngắt an toàn này khi nó không thể giải quyết một kiểu dữ liệu đủ nhanh. Bạn có thể gặp phải tình trạng này trong ba tình huống cụ thể:
- Cây dữ liệu sâu: Xử lý các cấu trúc JSON đệ quy từ một CMS cũ hoặc cây thư mục phức tạp.- Union lớn: Sử dụng các mapped type lặp qua các union có hơn 50 hoặc 100 thành phần.- Phụ thuộc vòng (Circular Dependencies): Kiểu A tham chiếu đến Kiểu B, kiểu B lại trỏ ngược về Kiểu A, tạo ra một vòng lặp mà trình biên dịch không thể làm phẳng.## Các giải pháp hiệu quả### 1. Sử dụng Interface để trì hoãn việc đánh giáCác bí danh kiểu (
type) có tính chất "eager" (đánh giá ngay lập tức). Trình biên dịch cố gắng giải quyết chúng ngay. Tuy nhiên, interface lại có tính chất "lazy" (đánh giá lười). Bằng cách bao bọc phần logic đệ quy bên trong một interface, bạn thường có thể đặt lại bộ đếm độ sâu. Trình biên dịch sẽ không cố gắng giải quyết toàn bộ cấu trúc cho đến khi bạn thực sự sử dụng một thuộc tính cụ thể.
// ❌ Điều này gây ra lỗi nếu T lồng nhau quá sâu
type DeepWrap<T> = {
data: T;
inner: DeepWrap<T>;
};
// ✅ Sử dụng interface để ép buộc đánh giá lười
interface DeepWrap<T> {
data: T;
inner: DeepWrap<T>;
}
2. Triển khai bộ đếm độ sâu thủ côngKhi xây dựng các utility type như DeepReadonly, bạn có thể giới hạn đệ quy bằng cách truyền một tham số "nhiên liệu" (fuel). Sử dụng bộ đếm dựa trên tuple để theo dõi trình biên dịch đã đi sâu bao nhiêu cấp độ. Khi nhiên liệu cạn kiệt, quá trình đệ quy sẽ dừng lại.
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
type DeepResolve<T, D extends number = 5> =
D extends 0
? T
: T extends object
? { [K in keyof T]: DeepResolve<T[K], Prev[D]> }
: T;
// Điều này sẽ chỉ đệ quy sâu 5 cấp, ngăn trình biên dịch bị treo
type SafeType = DeepResolve<MyComplexObject, 5>;
3. Tận dụng Tail-Call Optimization (TCO)TypeScript 4.5 đã giới thiệu đệ quy đuôi (tail-recursion) cho các kiểu điều kiện (conditional types). Nếu lời gọi đệ quy là thao tác cuối cùng trong logic của bạn, trình biên dịch có thể tối ưu hóa nó. Điều này giúp tăng giới hạn của bạn từ khoảng 50 cấp lên gần 1.000 cấp.
// ❌ Không phải đệ quy đuôi: toán tử spread xảy ra SAU khi đệ quy
type Reverse<T extends any[]> = T extends [infer Head, ...infer Tail]
? [...Reverse<Tail>, Head]
: [];
// ✅ Đệ quy đuôi: đệ quy là thao tác cuối cùng
type ReverseTCO<T extends any[], Acc extends any[] = []> = T extends [infer Head, ...infer Tail]
? ReverseTCO<Tail, [Head, ...Acc]>
: Acc;
4. Giải pháp thoát hiểm bằng "Any"Đôi khi bạn bị mắc kẹt với một thư viện bên thứ ba hoặc mã nguồn cũ mà bạn không thể tái cấu trúc. Trong những trường hợp này, hãy phá vỡ chuỗi đệ quy bằng cách ép kiểu một phân đoạn trung gian sang any. Đây là một công cụ thô bạo, nhưng nó sẽ dừng vòng lặp vô hạn ngay lập tức.
type ComplexMapper<T> = T extends string
? string
: T extends object
? { [K in keyof T]: ComplexMapper<any> } // Ngăn trình biên dịch đào sâu hơn
: T;
Xác minh giải phápSau khi sửa các kiểu dữ liệu, hãy xác minh tính ổn định của bản build bằng ba bước sau:
- Kiểm tra Terminal: Chạy
npx tsc --noEmit. Nếu nó vượt qua mà không có lỗi, giải pháp cấu trúc của bạn đã ổn định.- Kiểm tra Intellisense: Di chuột qua các biến trong VS Code. Nếu kiểu dữ liệu hiển thịanyhoặc...quá sớm, bộ đếm độ sâu của bạn có thể đang được đặt quá thấp.- Kiểm tra kiểu (Type Testing): Sử dụngtsdhoặcexpect-typeđể xác nhận kiểu dữ liệu vẫn trả về đúng hình dạng mong muốn.``` import { expectType } from 'tsd';
// Xác minh rằng phiên bản TCO của chúng ta xử lý mảng chính xác expectType<ReverseTCO<[1, 2, 3]>>([3, 2, 1]);
## Lời khuyên chủ độngTránh áp dụng các kiểu đệ quy cho các cấu trúc khổng lồ không xác định như phản hồi API thô. Thay vào đó, hãy xác định các ranh giới rõ ràng. Nếu bạn đang xuất bản một thư viện, hãy luôn cung cấp các lựa chọn thay thế "nông" (shallow) cho các utility type của mình. Điều này đảm bảo người dùng có cấu trúc dữ liệu khổng lồ không bị ảnh hưởng bởi giới hạn đệ quy của bạn.

