TypeScript – Tận dụng discriminated unions + never để code vừa an toàn vừa sạch

TypeScript "tự động" báo lỗi khi quên xử lý case — có tin nổi không?
Chào mấy bạn, mình là đây.
Hôm nay ngồi nhớ lại chuyện hồi mới chuyển từ JavaScript qua TypeScript, thấy buồn cười quá nên viết bài này chia sẻ.
Hồi đó mình cứ tưởng TypeScript chỉ để ghi kiểu cho biến, function, rồi thỉnh thoảng bị TS chửi "Type 'undefined' is not assignable to type..." là xong. Nhưng mà dùng riết rồi mới thấy cái hay nhất của TS hông phải là kiểu cơ bản, mà là discriminated unions với exhaustive checking – hai thứ này kết hợp vô làm code mình ở một đẳng cấp khác luôn.
Ảnh: Lukas Blazek — Pexels
Discriminated Union là gì mà ghê vậy?
Nói nôm na là mình định nghĩa một kiểu mà mỗi "biến thể" đều có chung một thuộc tính để phân biệt (thường gọi là type hay kind). Ví dụ:
type APIState =
| { status: 'idle' }
| { status: 'loading'; startTime: Date }
| { status: 'success'; data: string[] }
| { status: 'error'; message: string }
Giờ mình muốn render UI dựa trên state này, chỉ cần switch theo status là TS tự động biết mỗi case có những field nào — không lo quên, không lo sai.
Cái hay qua trọng là: nếu sau này thêm một biến thể mới, ví dụ { status: 'paused' }, TypeScript sẽ báo lỗi ngay ở chỗ switch cũ, nhắc mình cập nhật. Hông cần nhớ, hông cần tracking, code tự nhắc.
Mẹo nhỏ: dùng never để TS làm "cảnh sát giao thông"
Khi viết switch/case (hoặc if/else chain), hãy thêm một default handler kiểu:
function render(state: APIState) {
switch (state.status) {
case 'idle': return <Spinner />;
case 'loading': return <Loader startedAt={state.startTime} />;
case 'success': return <List data={state.data} />;
case 'error': return <ErrorBanner message={state.message} />;
default:
const _exhaustive: never = state;
return _exhaustive;
}
}
Dòng const _exhaustive: never = state; chính là "cảnh sát" đó. Nếu quên xử lý một case, TS sẽ báo lỗi ngay — vì state lúc đó thuộc kiểu không được khai báo, còn never thì không nhận cái gì hết. Compile-time safety thay vì runtime bug. Dân hay gọi cái này là "making illegal states unrepresentable."
Ảnh: Sarah — Pexels
Áp dụng vô dự án thực tế thấy ngay lợi
Hồi trước mình viết một cái payment flow có 6–7 trạng thái: pending, processing, success, failed, refunded, chargeback. Nếu dùng discriminated union, ai đọc code cũng biết ngay trạng thái nào có field gì. Thêm hay bớt trạng thái cũng không sợ sót — TypeScript lo phần còn lại. Code review nhẹ hẳn, mental load giảm rõ.
Cái hay hông dừng ở frontend đâu. Dân backend Node.js hay Rust cũng xài pattern này nhiều lắm. Rust có enum với match bắt buộc exhaustive — cơ mà với TypeScript + never, tụi mình cũng làm được gần tương tự.
Kết
Nếu bạn đang code TypeScript mà chưa dùng discriminated unions, thử ngay tuần này đi. Bắt đầu với cái gì nhỏ như API state, loading state, rồi từ từ mở rộng ra. Bảo đảm code sạch hơn, bug ít hơn, và mỗi lần thêm tính năng không còn sợ "chỗ này đã xử lý hết chưa ta?"
Còn nếu bạn đã xài rồi, cho mình hỏi: bạn dùng pattern này ở đâu thấy ngon nhất? Comment bên dưới nha.
Chúc mấy bạn cuối tuần vui vẻ và code ít bug. 🙌