- Viết được conditional types dạng
T extends U ? X : Yvà hiểu phân phối qua union. - Master keyword
infer— trích xuất type ẩn bên trong generic. - Tạo mapped types với key remapping (
as) và modifier (+readonly,-?). - Dùng template literal types để xây dựng kiểu string từ literal + biến.
- Viết recursive types như
DeepReadonly,DeepPartial— và biết giới hạn ~50 tầng. - Đọc được type definition của thư viện thật (Awaited, Prisma's
Args). - Nhận biết khi type "quá phức tạp" — dừng đúng lúc, đừng over-engineer.
1. Conditional types — "ternary" ở tầng type
Bạn đã biết condition ? a : b ở runtime. TypeScript có dạng tương tự ở tầng type:
T extends U ? X : Y. Đọc là: "nếu type T assignable to U thì chọn X,
không thì chọn Y".
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
// Ví dụ thực tế — chọn loại response
type ApiResponse<T> = T extends Error
? { ok: false; error: T['message'] }
: { ok: true; data: T };
type Good = ApiResponse<{ id: number }>;
// { ok: true; data: { id: number } }
type Bad = ApiResponse<Error>;
// { ok: false; error: string }
Đừng đọc T extends U là "kế thừa". Ở tầng type, extends nghĩa là compatible
with — "T có thể gán cho U không?". Ví dụ 'hello' extends string
đúng (literal là sub-type của string), nhưng string extends 'hello' sai.
1.1. Distributive conditional — phân phối qua union
Đây là tính chất quan trọng: khi T là union và xuất hiện "bare" (trần) ở vế trái
extends, TS sẽ phân phối — áp dụng conditional cho từng thành phần rồi gộp lại.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string>;
// string[]
type B = ToArray<string | number>;
// TS phân phối:
// ToArray<string> | ToArray<number>
// = string[] | number[]
// CHỨ KHÔNG PHẢI (string | number)[]
Muốn tắt phân phối: bọc cả 2 vế bằng tuple [T].
type ToArrayBundled<T> = [T] extends [any] ? T[] : never;
type C = ToArrayBundled<string | number>;
// (string | number)[] ← gộp lại, không phân phối
// Use case thực tế: check chính xác type, không bị "rớt qua kẽ":
type IsExactlyString<T> = [T] extends [string] ? true : false;
type X = IsExactlyString<string | number>; // false (vì có number)
type Y = IsExactlyString<string>; // true
Phân phối chỉ xảy ra khi type tham số đứng trần ở vế trái: T extends U.
Nếu bạn viết T & {} extends U hay [T] extends [U] thì không phân phối.
Đây là kỹ thuật để "bật / tắt" theo ý.
2. Keyword infer — bắt type ẩn
infer như "biến trống" ở tầng type — bạn nói TS: "ở chỗ này có một type, hãy gán nó cho biến tên
R để tôi dùng". infer chỉ hợp lệ ở vế phải extends trong conditional type.
// Trích kiểu trả về của function
type MyReturnType<F> = F extends (...args: any[]) => infer R
? R
: never;
type A = MyReturnType<() => number>; // number
type B = MyReturnType<(s: string) => boolean>; // boolean
type C = MyReturnType<string>; // never (không match)
Bạn có thể đặt infer ở rất nhiều vị trí. Vài ví dụ:
// Lấy type đầu tiên của tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number, boolean]>; // string
// Lấy phần còn lại (tail)
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : [];
type T1 = Tail<[string, number, boolean]>; // [number, boolean]
// Unwrap Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type U = UnwrapPromise<Promise<number>>; // number
type V = UnwrapPromise<string>; // string (không phải Promise → trả nguyên)
// Lấy type element của array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<number[]>; // number
Nếu đã học ngôn ngữ functional (Haskell, OCaml), infer chính là pattern matching trên type.
Bạn viết "khuôn" và TS điền vào chỗ trống. Tương tự destructuring nhưng ở compile-time.
2.1. Trace ví dụ — TS đánh giá conditional thế nào
Hãy theo dõi UnwrapPromise<Promise<Promise<number>>> bước-bước:
// Định nghĩa:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Bước 1: thay T = Promise<Promise<number>>
// Promise<Promise<number>> extends Promise<infer U> ?
// Bước 2: match — TS gán U = Promise<number>
// Bước 3: return U = Promise<number>
// Vậy UnwrapPromise một lần CHỈ unwrap 1 tầng.
// Muốn unwrap nhiều tầng → đệ quy (recursive type, xem mục 9).
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;
// ↑ tự gọi lại
type R = Awaited2<Promise<Promise<number>>>; // number
// Bước 1: match → U = Promise<number>, gọi Awaited2<Promise<number>>
// Bước 2: match → U = number, gọi Awaited2<number>
// Bước 3: không match Promise → trả T = number. DONE.
Đây chính xác là cách Awaited<T> built-in của TS hoạt động (có thêm xử lý .then).
3. Mapped types — duyệt key, biến hình value
Mapped type là cách "loop" qua keys của một type và sinh ra type mới. Cú pháp:
{ [K in keyof T]: ... }. Bạn đã thấy ở chương 10 (Partial, Readonly). Giờ ta đào sâu.
// Đánh dấu tất cả property là readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
interface User { id: number; name: string; }
type ReadonlyUser = MyReadonly<User>;
// {
// readonly id: number;
// readonly name: string;
// }
// Biến value thành Promise của value
type Asyncify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Asyncify<User>;
// { id: Promise<number>; name: Promise<string>; }
3.1. Modifier + và -
Mặc định mapped type là homomorphic — giữ nguyên modifier readonly và ?
của source. Bạn có thể thêm (+) hoặc bỏ (-) modifier tường minh:
// Bỏ readonly khỏi mọi property
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Locked = { readonly a: number; readonly b: string };
type Unlocked = Mutable<Locked>;
// { a: number; b: string } ← bỏ readonly
// Bỏ optional khỏi mọi property → bắt buộc
type Required2<T> = {
[K in keyof T]-?: T[K];
};
type Loose = { a?: number; b?: string };
type Strict = Required2<Loose>;
// { a: number; b: string } ← bỏ ?
// Thêm optional cho mọi property
type Partial2<T> = {
[K in keyof T]+?: T[K]; // +? cũng chỉ là ?
};
+readonly và +? hiếm khi viết tường minh vì là default. Bạn sẽ thấy chủ yếu là
-readonly và -? — chúng là 2 modifier "bóc lớp" hữu ích nhất.
3.2. Key remapping với as
Từ TS 4.1, bạn có thể đổi tên key bằng as. Đây là tính năng mở khoá rất nhiều
pattern mới — đặc biệt khi kết hợp với template literal.
// Sinh getter từ field — name → getName
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
interface User { id: number; name: string; email: string; }
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
Một use case rất phổ biến: lọc key bằng never:
// Giữ key của field "string", bỏ field khác
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
// ↑ remap về never = loại bỏ key đó
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type OnlyStrings = StringKeys<Mixed>;
// { name: string; email: string }
Trong mapped type với as, nếu key remap về never, TS hiểu là "bỏ key này".
Đây là "trick" chuẩn của TypeScript để filter / pick có điều kiện.
4. Template literal types — string ở tầng type
Template literal type cho phép xây dựng kiểu string từ literal + type variable. Cú pháp giống template literal runtime, nhưng ở tầng type.
type Greeting = `Hello, ${string}!`;
const g1: Greeting = 'Hello, world!'; // ✅
const g2: Greeting = 'Hi'; // ❌ Lỗi
type Route = `/users/${string}`;
const r1: Route = '/users/42'; // ✅
const r2: Route = '/posts/42'; // ❌
// Kết hợp với union → tổ hợp string
type Color = 'red' | 'green' | 'blue';
type Variant = 'light' | 'dark';
type Token = `${Variant}-${Color}`;
// 'light-red' | 'light-green' | 'light-blue' |
// 'dark-red' | 'dark-green' | 'dark-blue'
4.1. Built-in template helpers
TS có sẵn 4 helper biến đổi string type:
| Helper | Input | Output |
|---|---|---|
Uppercase<T> | 'hello' | 'HELLO' |
Lowercase<T> | 'HELLO' | 'hello' |
Capitalize<T> | 'hello' | 'Hello' |
Uncapitalize<T> | 'Hello' | 'hello' |
// Convert keys snake_case → camelCase
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S;
type A = CamelCase<'first_name'>; // 'firstName'
type B = CamelCase<'created_at_utc'>; // 'createdAtUtc'
// Áp dụng cho object
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<K & string>]: T[K];
};
type DBRow = { first_name: string; created_at: Date };
type JsRow = CamelCaseKeys<DBRow>;
// { firstName: string; createdAt: Date }
5. keyof và indexed access — nền tảng
Hai operator bạn đã thấy nhiều lần. Tóm tắt lại đầy đủ:
interface User { id: number; name: string; email: string }
// keyof T = union các key
type K = keyof User; // 'id' | 'name' | 'email'
// T[K] = lookup type của value tại key K
type IdType = User['id']; // number
// T[keyof T] = union tất cả value type
type AllValues = User[keyof User]; // number | string
// Trên array
type Arr = [number, string, boolean];
type First = Arr[0]; // number
type All = Arr[number]; // number | string | boolean
// Trên union
type Action = { kind: 'add'; n: number } | { kind: 'reset' };
type Kinds = Action['kind']; // 'add' | 'reset'
Thay vì khai báo lại type, dùng indexed access để "lấy ra" từ source:
// ❌ duplicate
function getName(u: User): string { ... }
// ✅ lấy từ source — đổi User là auto đổi
function getName(u: User): User['name'] { ... }
6. Recursive types — đệ quy ở tầng type
Type có thể tự gọi lại chính nó. Đây là cách viết được type đi sâu vào object lồng nhau (deep) như
DeepReadonly, DeepPartial, hoặc parse string phức tạp.
type DeepReadonly<T> = {
readonly [K in keyof T]:
T[K] extends object
? DeepReadonly<T[K]> // đệ quy nếu là object
: T[K]; // primitive — giữ nguyên
};
interface Config {
api: {
url: string;
auth: { token: string; refreshIn: number };
};
debug: boolean;
}
type FrozenConfig = DeepReadonly<Config>;
// {
// readonly api: {
// readonly url: string;
// readonly auth: {
// readonly token: string;
// readonly refreshIn: number;
// };
// };
// readonly debug: boolean;
// }
TS chống vòng lặp vô hạn bằng "recursion depth limit". Hard limit khoảng 50 tầng (tuỳ phiên bản). Vượt qua sẽ báo "Type instantiation is excessively deep and possibly infinite".
Với object thực tế (config, nested form), hiếm khi chạm 50 tầng. Nhưng parser string đệ quy (vd: parse JSON ở tầng type) dễ chạm. Khi đó: thiết kế lại bằng tail recursion accumulator hoặc giảm độ sâu.
7. Đọc một utility type từ thư viện
Bạn cần kỹ năng đọc type — không chỉ viết. Hãy phân tích Awaited built-in của TS
(rút gọn từ lib.es5.d.ts):
type Awaited<T> =
T extends null | undefined
? T // ① giữ nguyên null/undefined
: T extends object & { then(onfulfilled: infer F, ...): any }
? F extends ((value: infer V, ...) => any)
? Awaited<V> // ② nếu thenable, lấy value V và đệ quy
: never // ③ thenable nhưng then không là function
: T; // ④ không phải Promise / thenable → giữ nguyên
Đọc step-by-step (đây chính là kỹ năng cần luyện):
- Dòng ①: Edge case. Nếu
Tlànullhayundefined, trả nguyên (không await được). - Dòng ②: Pattern match thenable — object có method
.then(onfulfilled, ...). Nếuonfulfilledlà function nhậnvalue: V, thì lấyVvà đệ quy. Đây là lý doawait"phẳng hoá" Promise lồng nhau. - Dòng ③: Trường hợp lạ — có
.thennhưng không phải function. Trảnever. - Dòng ④: Default — không phải Promise →
awaittrả luôn giá trị đó.
Việc đọc được code này = bạn đã sẵn sàng đọc Prisma.UserGetPayload, zod.infer,
tRPC.RouterOutput.
8. Anti-pattern — khi nào dừng lại
Type-level programming rất "addictive" — viết được type cực hay khiến bạn muốn xây cả compiler ở tầng type. Đừng. Đây là vài dấu hiệu type của bạn đã over-engineered:
| Dấu hiệu | Tác hại | Khắc phục |
|---|---|---|
| Conditional lồng nhau >5 tầng | Người đọc bỏ cuộc sau 2 phút | Tách thành nhiều type alias, đặt tên rõ |
Chain infer dài | Khó debug, hover không hiện | Refactor sang helper type ngắn hơn |
tsc mỗi check >100ms | IDE lag, autocomplete chậm | Dùng --extendedDiagnostics để tìm thủ phạm |
| Hover hiển thị type quá dài | Developer không hiểu type gợi ý là gì | Wrap bằng named type để IDE hiển thị tên |
| Sửa nhỏ → cả file đỏ | Coupling giữa các type quá chặt | Tách module, dùng interface thay vì type khi có thể |
| JSDoc giải thích còn dễ hơn type | Type không còn đóng vai trò "tài liệu" | Đổi sang JSDoc + type đơn giản |
Type chỉ nên giúp bạn tránh bug và tự document. Khi nó trở thành thử thách trí tuệ riêng → bạn đang viết "type-level Sudoku", không phải code production. Quy tắc của tác giả ts-toolbelt (Pierre Antoine): "Nếu cần >30 phút để giải thích type cho đồng nghiệp, viết lại."
8.1. Đo tốc độ compile
Khi nghi ngờ type chậm:
# Hiện chi tiết thời gian
$ tsc --extendedDiagnostics
# Hiện file nào chậm nhất
$ tsc --generateTrace ./trace
# Mở Chrome DevTools → Performance → load trace.json
# Tìm "checkTypeRelatedTo" — top vài entry là thủ phạm
# Ngưỡng tham khảo (project medium):
# - Total: <5s OK, 5-20s acceptable, >20s cần optimize
# - Single file check: <100ms OK, >500ms suspicious
9. Ví dụ trace — viết DeepPartial step-by-step
Hãy viết DeepPartial rồi trace cách TS suy luận.
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
interface Form {
user: {
name: string;
address: { city: string; zip: string };
};
active: boolean;
}
type PartialForm = DeepPartial<Form>;
// Trace cách TS suy ra:
// Bước 1: K duyệt 'user' | 'active'
// Bước 2: K='user' → T[K] = { name, address }
// { name, address } extends object? YES
// → DeepPartial<{ name, address }>
// - K duyệt 'name' | 'address'
// - K='name' → T[K]=string. string extends object? NO → giữ string
// - K='address' → T[K]={ city, zip }. YES object → DeepPartial<...>
// - K='city' → string → giữ
// - K='zip' → string → giữ
// Kết quả: { city?: string; zip?: string }
// Kết quả: { name?: string; address?: { city?: string; zip?: string } }
// Bước 3: K='active' → T[K]=boolean. boolean extends object? NO → giữ
// Final:
// {
// user?: {
// name?: string;
// address?: { city?: string; zip?: string };
// };
// active?: boolean;
// }
Wrap kết quả bằng Prettify<T> = { [K in keyof T]: T[K] } & {} để IDE expand
"phẳng" thay vì hiển thị DeepPartial<...> trừu tượng. Pattern này dùng đầy ở
Prisma, zod.
10. Kết nối với phần khác
Utility built-in ở chương 10 (Partial, Pick, Omit...)
chính là ứng dụng cụ thể của các kỹ thuật chương này:
// Partial = mapped + modifier ?
type Partial<T> = { [P in keyof T]?: T[P] };
// Required = mapped + remove ?
type Required<T> = { [P in keyof T]-?: T[P] };
// Pick = mapped + giới hạn K
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Exclude = distributive conditional
type Exclude<T, U> = T extends U ? never : T;
// ReturnType = infer trong conditional
type ReturnType<F extends (...args: any) => any> =
F extends (...args: any) => infer R ? R : never;
Bài tập
Bài 1 — Tự viết Awaited<T>
Viết type MyAwaited<T> "phẳng hoá" Promise lồng nhau. Test:
MyAwaited<Promise<number>>→numberMyAwaited<Promise<Promise<string>>>→stringMyAwaited<Promise<Promise<Promise<boolean>>>>→booleanMyAwaited<number>→number(không phải Promise → trả nguyên)
Đáp án + giải thích
type MyAwaited<T> = T extends Promise<infer U>
? MyAwaited<U>
: T;
// Test
type A = MyAwaited<Promise<number>>;
// Bước 1: match Promise<infer U>, U=number → gọi MyAwaited<number>
// Bước 2: number không match Promise → trả number. OK.
type B = MyAwaited<Promise<Promise<string>>>;
// Bước 1: U=Promise<string> → gọi MyAwaited<Promise<string>>
// Bước 2: U=string → gọi MyAwaited<string>
// Bước 3: string không match → trả string. OK.
So với Awaited built-in: built-in phức tạp hơn vì còn xử lý thenable
(object có method .then nhưng không phải instance của Promise) và null/undefined.
Bài 2 — DeepPartial<T> với 3 tầng
Viết DeepPartial và test với 3 tầng lồng:
interface Settings {
ui: {
theme: { primary: string; secondary: string };
layout: string;
};
api: { url: string; timeout: number };
}
Output mong đợi: mọi field đều optional, kể cả nested.
Đáp án
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
type PartialSettings = DeepPartial<Settings>;
// Sử dụng
const override: PartialSettings = {
ui: {
theme: { primary: '#fff' } // chỉ override primary, không cần secondary
}
};
Lưu ý gotcha: function cũng là object trong TS, nên
(...) => void sẽ bị DeepPartial áp dụng — thường không mong muốn. Fix bằng cách thêm điều kiện:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends Function ? T[K]
: T[K] extends object ? DeepPartial<T[K]>
: T[K];
};
Bài 3 — PathParams<T> parse route Express-style
Viết type PathParams<T> nhận route string dạng Express, trả object với key là tên param.
PathParams<'/users/:id'>→{ id: string }PathParams<'/users/:id/posts/:postId'>→{ id: string; postId: string }PathParams<'/health'>→{}
Đáp án + giải thích
type PathParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof PathParams<`/${Rest}`>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
// Test
type A = PathParams<'/users/:id'>;
// { id: string }
type B = PathParams<'/users/:id/posts/:postId'>;
// { id: string; postId: string }
type C = PathParams<'/health'>;
// {}
Cách hoạt động: chia route làm 2 trường hợp:
- Có ít nhất 1 dấu
/sau param: bắtParamrồi đệ quy phầnRest. - Param ở cuối (không có
/sau): chỉ bắtParam. - Không có
:: trả{}.
Đây là pattern dùng trong tRPC và Next.js typed routes.
Bài 4 — Functions<T> chỉ giữ field là function
Viết type Functions<T> trả object chỉ có các property function-typed.
Hint: kết hợp mapped + conditional + key remap với never.
- Input
{ name: string; greet: () => void; age: number; bye: (s: string) => void } - Output
{ greet: () => void; bye: (s: string) => void }
Đáp án
type Functions<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any
? K
: never
]: T[K];
};
interface Mixed {
name: string;
greet: () => void;
age: number;
bye: (s: string) => void;
}
type OnlyFns = Functions<Mixed>;
// { greet: () => void; bye: (s: string) => void }
Use case: trích "method" của một class hoặc generate "RPC interface" từ object.
Bài 5 — Đọc utility từ thư viện (Prisma / Zod)
Mở node_modules/zod/lib/types.d.ts hoặc
node_modules/@prisma/client/runtime/library.d.ts. Tìm một utility type (đề xuất:
z.infer, Prisma.GetResult, Prisma.Args).
Copy code, dán vào file riêng và comment từng dòng giải thích. Mục tiêu là không học thuộc — chỉ rèn kỹ năng đọc.
Ví dụ — phân tích z.infer
// zod/lib/types.d.ts (rút gọn)
type infer<T extends ZodType<any, any, any>> = T['_output'];
// ↑ ↑
// - generic T phải là một ZodType (schema validator).
// - lookup property internal '_output' (TS phantom field).
// Kết quả: lấy type "output sau parse" của schema.
// Ví dụ:
const UserSchema = z.object({ id: z.number(), name: z.string() });
// UserSchema._output = { id: number; name: string }
type User = z.infer<typeof UserSchema>;
// = UserSchema['_output']
// = { id: number; name: string }
Pattern "phantom field" (_output) là cách giữ type tham số mà không tạo runtime overhead.
Cũng dùng nhiều trong tRPC, GraphQL codegen.
Quiz
Cho type X = T extends string ? 1 : 0. Khi T = 'a' | 42, X là gì?
Xem đáp án
1 | 0. Distributive conditional: T đứng "trần" ở vế trái nên TS phân phối — áp dụng cho từng phần tử union. 'a' extends string → 1, 42 extends string → 0. Gộp lại: 1 | 0.
Làm sao tắt distributive conditional?
Xem đáp án
Bọc cả 2 vế bằng tuple: [T] extends [U]. Khi đó T không còn "trần" → không phân phối. Đây là cách so sánh "toàn bộ union một cục" thay vì từng phần tử.
infer dùng được ở đâu? Có dùng được ngoài conditional không?
Xem đáp án
Không. infer chỉ hợp lệ ở vế phải của extends trong conditional type. Dùng ngoài: lỗi "'infer' declarations are only permitted in the 'extends' clause of a conditional type".
Mapped type mặc định có giữ readonly và ? của source không?
Xem đáp án
Có (homomorphic). Mặc định mapped type giữ nguyên modifier readonly và ? của type gốc. Muốn thêm tường minh: +readonly, +?. Muốn bỏ: -readonly, -?.
Cho T = { a: 1; b: 2 }. keyof T là gì?
Xem đáp án
'a' | 'b' — union của các key (string literal). Còn T[keyof T] sẽ là 1 | 2 — union của các value type.
Template literal `/api/${string}` match string nào?
Xem đáp án
Bất kỳ string nào bắt đầu bằng '/api/' theo sau là chuỗi ký tự bất kỳ (kể cả rỗng). Ví dụ '/api/', '/api/users', '/api/users/42?q=1' đều match. '/api' (thiếu /) thì không.
Recursive type của TS có giới hạn không? Bao nhiêu tầng?
Xem đáp án
Có. Hard limit khoảng ~50 tầng (tuỳ phiên bản TS). Vượt qua → lỗi "Type instantiation is excessively deep and possibly infinite". Khi đụng: tái thiết kế bằng tail-recursion + accumulator, hoặc chấp nhận parse partial.
Tổng kết
Sau chương 11, bạn nên đã master:
- Conditional type
T extends U ? X : Y— "if/else" ở tầng type, có tính chất phân phối qua union. - Disable distributive bằng cách bọc tuple
[T] extends [U]. infer— bắt type ẩn, chỉ dùng trong conditional. Là "destructure ở tầng type".- Mapped type với modifier
+/-readonly,+/-?và key remap bằngas. - Template literal type — xây dựng string type động, kết hợp
Uppercase/Capitalize... - Recursive type để đi sâu vào structure (
DeepReadonly,DeepPartial) — nhớ limit ~50. - Đọc được type của thư viện thật (Awaited, Zod's
infer, Prisma). - Nhận biết khi type quá phức tạp — biết khi nào nên dừng lại.
Kết nối
- Chương 12 (TS Compiler & Tooling) — chương cuối TS: cấu hình tsconfig, build với esbuild/swc, type-only import, declaration files, project references.
- Chương 10 (TS Generics & Utility) — utility built-in như
Partial,Pickchính là ví dụ của mapped + conditional ở chương này. - Chương 4 (Objects & Class hierarchy) —
keyofvà indexed access trên class instance, cách TS suy ra type từthis.