DEV Community

Advanced Type — Conditional Type

Conditional type và infer: kiểu phụ thuộc kiểu khác

Conditional type cho phép chọn kiểu dựa trên một điều kiện kiểu, viết như toán tử ba ngôi: T extends U ? X : Y. Kết hợp với infer — trích một kiểu con từ bên trong một kiểu khác — nó là công cụ để viết utility type mạnh: bóc kiểu phần tử khỏi mảng, kiểu trả về khỏi hàm, kiểu resolve khỏi Promise. Đây là phần mạnh nhất và cũng nguy hiểm nhất của type system: nó khiến viết được thư viện type linh hoạt, nhưng cũng dễ tạo ra kiểu không ai bảo trì nổi và làm compile chậm.

Cơ chế hoạt động

Conditional type kiểm tra một quan hệ kiểu và trả về một trong hai nhánh; infer đặt một biến kiểu để TS tự suy phần cần trích:

type ElementType<T> = T extends (infer E)[] ? E : T
type A = ElementType<string[]>  // string — bóc kiểu phần tử
type B = ElementType<number>    // number — không phải mảng thì trả chính nó

type Awaited2<T> = T extends Promise<infer R> ? R : T // trích kiểu mà Promise resolve về
type C = Awaited2<Promise<User>> // User
Enter fullscreen mode Exit fullscreen mode

infer E nghĩa "nếu T khớp dạng (something)[], đặt somethingE rồi dùng E ở nhánh true". Conditional type còn phân phối trên union: T extends U ? ... : ... với T là union sẽ áp riêng cho từng thành phần rồi gộp lại — hành vi mạnh nhưng hay gây bất ngờ.

Vấn đề gặp trong production

Use case điển hình: utility type cho thư viện dùng chung. Conditional type tỏa sáng khi viết kiểu suy ra tự động cho một API generic, để người dùng thư viện không phải khai báo kiểu thủ công:

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> } // đệ quy: optional ở mọi cấp
  : T

type Config = { db: { host: string; port: number }; cache: { ttl: number } }
type PartialConfig = DeepPartial<Config> // mọi field ở mọi độ sâu thành optional
Enter fullscreen mode Exit fullscreen mode

DeepPartial hữu ích cho cấu hình có default sâu — người dùng chỉ override field họ cần. Đây là loại utility chính đáng để dùng conditional + recursive type.

Failure mode: compile chậm. Conditional type đệ quy hoặc phân phối trên union lớn buộc type checker làm nhiều việc; trong codebase lớn, vài type "thông minh" có thể kéo thời gian biên dịch và làm editor (IntelliSense) lag rõ rệt. Đây không phải lý thuyết — một DeepPartial/DeepReadonly áp lên cấu trúc rất sâu, hay một conditional type phân phối trên union hàng trăm thành phần, là nguồn chậm thật.

Failure mode: không ai bảo trì nổi. Một type như T extends (...args: infer A) => infer R ? ... : ... lồng nhiều tầng infer và conditional trở thành "magic" — người viết hiểu, cả nhóm còn lại thì không, và thông báo lỗi khi nó sai dài hàng chục dòng vô nghĩa. Power đi kèm chi phí bảo trì: chỉ nên đẩy độ phức tạp này vào lớp thư viện/utility nền, không rải vào code nghiệp vụ hằng ngày.

Cách debug và monitor

Khi một conditional type cho kết quả không như mong (đặc biệt với union — nhớ hành vi phân phối), gán nó vào một biến và hover để xem TS giải ra gì, hoặc dùng một type-level test (type _check = Expect<Equal<Result, Expected>>). Khi compile/editor chậm dần, chạy tsc --extendedDiagnostics xem thời gian check, và --generateTrace để tìm type tốn nhất — thường là một conditional/recursive type áp lên cấu trúc lớn. Quy ước review: conditional type phức tạp chỉ sống trong file utility type riêng, có test kiểu và comment giải thích ý định; không để chúng lẫn trong code domain.

Tradeoff

Conditional type + infer cực mạnh — suy kiểu tự động, viết utility tổng quát, giảm khai báo thủ công cho người dùng thư viện. Cái giá thẳng thừng: khó đọc, khó bảo trì, lỗi khó hiểu, và compile/editor chậm khi đệ quy hoặc phân phối trên union lớn. Quy tắc thực tế: dùng cho lớp thư viện/utility nền nơi sự tổng quát đáng giá và được test kỹ; tránh trong code nghiệp vụ nơi một kiểu cụ thể rõ ràng hơn nhiều; và luôn cân nhắc liệu một kiểu đơn giản hơn (kể cả phải khai báo thêm vài dòng) có dễ bảo trì hơn một conditional type thông minh không.

Câu hỏi phỏng vấn

infer là gì, và đánh đổi khi dùng conditional type phức tạp?

infer dùng trong một conditional type để trích một kiểu con từ bên trong kiểu đang kiểm tra: T extends Promise<infer R> ? R : T đặt R là kiểu mà Promise resolve về và dùng nó ở nhánh true; tương tự bóc kiểu phần tử mảng, kiểu trả về/tham số của hàm. Conditional type (T extends U ? X : Y) còn phân phối trên union — áp riêng từng thành phần rồi gộp. Đánh đổi khi dùng phức tạp: kiểu đệ quy/lồng nhiều tầng infer trở nên khó đọc và bảo trì, thông báo lỗi dài vô nghĩa, và làm chậm compile lẫn editor (đặc biệt khi phân phối trên union lớn hoặc đệ quy sâu). Điểm ăn điểm: giới hạn độ phức tạp này ở lớp thư viện/utility có test kiểu, dùng kiểu cụ thể trong code nghiệp vụ, và dùng --extendedDiagnostics/--generateTrace để tìm type gây chậm.

Hands-on

Viết một DeepPartial<T> đệ quy và áp lên một kiểu cấu hình lồng nhiều cấp thật, xác nhận mọi field ở mọi độ sâu thành optional và dùng nó cho một hàm nhận override cấu hình. Viết vài type-level test (Expect<Equal<…>>) cho ElementType, Awaited2, và DeepPartial để khẳng định chúng suy đúng, kể cả trên union (quan sát hành vi phân phối). Sau đó cố tình áp một conditional/recursive type lên một cấu trúc rất lớn hoặc một union nhiều thành phần, chạy tsc --extendedDiagnostics để đo thời gian check tăng lên, rồi đơn giản hóa và so sánh.

Top comments (0)