- TypeScript 5.5 (tháng 6/2024) giải quyết issue 7 năm tuổi với 500+ upvotes - filter callback tự động được infer là type predicate.
- Không cần viết (x): x is string thủ công nữa khi lọc null khỏi mảng.
- filter(Boolean) vẫn KHÔNG hoạt động với kiểu nguyên thủy do ngữ nghĩa if-and-only-if.
- Một team xóa 214 dòng type guard boilerplate chỉ trong 4 giờ nâng cấp.
TL;DR
- TypeScript 5.5 (tháng 6/2024) ra mắt inferred type predicates - tự động suy luận rằng một filter function đang thu hẹp kiểu dữ liệu.
- Vấn đề: trước đây
array.filter(x => x !== null)vẫn trả về(T | null)[]thay vìT[], dù null đã bị loại ra hoàn toàn. - Fix: TS 5.5 dùng control flow analysis để tự hiểu rằng callback là một type predicate - không cần annotation.
- Góc khuất:
filter(Boolean)vàx => !!xvẫn KHÔNG hoạt động với kiểu nguyên thủy (number, string) do ngữ nghĩa "if and only if". - Thực tế: một team đã xóa 214 dòng type guard boilerplate trong 4 giờ sau khi nâng cấp lên TS 5.5.

Vấn đề 7 năm chưa có lời giải
Bạn có một mảng (string | null)[] và muốn lọc bỏ các giá trị null. Code runtime hoạt động hoàn hảo - nhưng TypeScript lại không chịu nhận ra:
const items: (string | null)[] = ["hello", null, "world", null];
const filtered = items.filter(x => x !== null);
// TypeScript nghĩ: (string | null)[] -- sai!
// Thực tế: string[] -- đúng
TypeScript coi callback của .filter() chỉ là một hàm trả về boolean - không hiểu rằng khi callback trả về true, giá trị đó chắc chắn không còn là null. Compiler bảo thủ: nó giữ nguyên kiểu gốc (string | null)[].
Vấn đề này được mở dưới dạng issue #16069 vào ngày 24 tháng 5, 2017 - tích lũy hơn 500 upvote trong gần 7 năm. Workaround cũ nhất là viết inline type predicate:
// Cách cũ - phải khai báo thủ công
const filtered = items.filter((x): x is string => x !== null);
Vấn đề: type guard thủ công là unsafe - giống như type assertion as, TypeScript không kiểm tra xem logic bên trong có khớp với type bạn assert hay không. Bạn có thể viết value is number nhưng thực tế check typeof value === 'string' và compiler không phàn nàn gì. Guard tay còn dễ bị drift khỏi interface khi code thay đổi.
TypeScript 5.5 làm gì khác
TypeScript 5.5 (release ngày 20 tháng 6, 2024) giới thiệu inferred type predicates: compiler tự dùng control flow analysis để suy luận rằng một hàm đang thu hẹp kiểu dữ liệu - không cần bạn khai báo thủ công.
// TypeScript 5.5+ - hoạt động tự nhiên
const filtered = items.filter(x => x !== null);
// filtered: string[] -- đúng rồi!
Phía sau hậu trường, TypeScript suy luận rằng hàm callback có signature (x: string | null): x is string. Vì Array.prototype.filter được typed để hiểu type predicates, kết quả cuối cùng là string[]. Feature này được implement bởi Dan Vanderkam (danvk) - chính là người mở issue #16069 năm 2017, mất gần 7 năm để quay lại fix. PR được merge ngày 15/3/2024.
4 điều kiện để inference hoạt động
TypeScript chỉ suy luận type predicate khi hàm thỏa đủ 4 điều kiện:
- Không có explicit return type hoặc type predicate annotation
- Chỉ có một return statement duy nhất, không có implicit returns
- Không mutate tham số đầu vào
- Return một boolean expression gắn với refinement của tham số
Các ví dụ được infer tự động:
x => x !== null // inferred: x is NonNullable<T>
x => typeof x === 'string' // inferred: x is string
x => x instanceof Date // inferred: x is Date
Trước TS 5.5, tất cả các hàm trên chỉ được infer là boolean. Bây giờ TypeScript infer đúng là x is string, x is Date, v.v.
Bẫy phổ biến nhất: truthiness checks
Đây là điểm dễ gây nhầm lẫn nhất. filter(Boolean) và x => !!x vẫn KHÔNG hoạt động với kiểu nguyên thủy như number | null:
const scores: (number | null)[] = [100, null, 0, 85, null];
const valid = scores.filter(x => !!x);
// valid: (number | null)[] -- vẫn sai!
Lý do: TypeScript áp dụng ngữ nghĩa "if and only if". Để infer predicate x is number, TypeScript phải đảm bảo: nếu hàm trả về false thì x chắc chắn KHÔNG phải number. Nhưng với !!x, khi trả về false, x có thể là null hoặc 0 - một số hợp lệ. TypeScript từ chối infer, và đúng vậy: nếu infer thì sẽ lọc luôn cả điểm 0 hợp lệ của học sinh!
Giải pháp: dùng explicit null check thay vì truthiness:
const valid = scores.filter(x => x !== null);
// valid: number[] -- đúng rồi, kể cả 0
Truthiness checks CHỈ hoạt động với object types (không có falsy ambiguity như 0 hay ""):
const birds: (Bird | undefined)[] = [...];
const realBirds = birds.filter(x => !!x);
// realBirds: Bird[] -- đúng vì object không có falsy trùng lặp
So sánh trước và sau TypeScript 5.5
| Cách filter | TS < 5.5 | TS 5.5+ |
|---|---|---|
.filter(x => x !== null) | (T | null)[] sai | T[] đúng |
.filter((x): x is T => x !== null) | T[] (thủ công) | T[] (vẫn hoạt động) |
.filter(Boolean) | (T | null)[] | (T | null)[] với primitives |
.filter(x => !!x) | (T | null)[] | T[] chỉ với object types |
Khi nào vẫn cần type guard thủ công
Inferred type predicates không phủ hết mọi trường hợp. Bạn vẫn cần viết value is Type khi:
- Logic phức tạp nhiều bước hoặc hàm có nhiều return statements
- Optional chaining:
x => x?.enabledkhông infer vì khi false,xcó thể lànullhoặc mộtDevicehợp lệ vớienabled = false - Public library API: explicit predicate giúp intent rõ ràng hơn với người dùng downstream
- Logic dựa vào external state hoặc async
Rule of thumb: nếu hàm chỉ có một return statement là một check đơn giản, để TypeScript tự infer. Còn lại thì explicit.
Tổng kết
TypeScript 5.5 inferred type predicates là một trong những feature hiếm hoi vừa xóa code vừa tăng type safety. Sau khi nâng cấp:
- Dùng
.filter(x => x !== null)thay vì.filter((x): x is T => x !== null) - Tránh
filter(Boolean)với number/string - dùng explicit null check - Xem lại ESLint config nếu có rule
explicit-function-return-type
Nguồn: TypeScript 5.5 announcement, The Making of a TypeScript Feature - Dan Vanderkam, Real-world migration case study.

