Chương 11 · TypeScript Advanced Types

TypeScript ở cấp độ wizard — type-level programming

Bây giờ bạn biết generics. Chương này là "lập trình ở tầng type" — viết được type tự suy luận như ReturnType, Awaited, DeepPartial. Hiểu rồi sẽ đọc được source của Prisma, Zod, tRPC — những thư viện modern dùng TS như ngôn ngữ lập trình thật sự. Cẩn thận anti-pattern: type quá phức tạp làm tsc chậm và IDE lag.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 9-10
🎯 Mục tiêu chương
  • Viết được conditional types dạng T extends U ? X : Y và 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".

Conditional type cơ bản
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 }
🧠 Mental model — đọc conditional

Đừ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.

Distributive — quan trọng
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].

Disable distributive
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
🔥 Gotcha — chỉ "bare" type mới phân phối

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.

infer — ReturnType
// 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ụ:

infer ở nhiều vị trí
// 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
🧠 Mental model — infer là pattern matching

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:

Trace step-by-step
// Đị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.

Mapped type cơ bản
// Đá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 +-

Mặc định mapped type là homomorphic — giữ nguyên modifier readonly? của source. Bạn có thể thêm (+) hoặc bỏ (-) modifier tường minh:

Add / remove modifier
// 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à ?
};
💡 Quy ước viết

+readonly+? hiếm khi viết tường minh vì là default. Bạn sẽ thấy chủ yếu là -readonly-? — 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.

Key remap with `as`
// 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:

Filter keys với 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 }
🧠 Mental model — never xoá key

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.

Template literal cơ bản
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:

HelperInputOutput
Uppercase<T>'hello''HELLO'
Lowercase<T>'HELLO''hello'
Capitalize<T>'hello''Hello'
Uncapitalize<T>'Hello''hello'
Template helpers + key remap
// 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 đủ:

keyof và T[K]
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'
💡 Pattern: indexed access tránh duplicate

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.

DeepReadonly recursive
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;
// }
🔥 Giới hạn đệ quy — ~50 tầng

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):

Awaited<T> built-in (rút gọn)
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):

  1. Dòng ①: Edge case. Nếu Tnull hay undefined, trả nguyên (không await được).
  2. Dòng ②: Pattern match thenable — object có method .then(onfulfilled, ...). Nếu onfulfilled là function nhận value: V, thì lấy V và đệ quy. Đây là lý do await "phẳng hoá" Promise lồng nhau.
  3. Dòng ③: Trường hợp lạ — có .then nhưng không phải function. Trả never.
  4. Dòng ④: Default — không phải Promise → await trả 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ệuTác hạiKhắc phục
Conditional lồng nhau >5 tầngNgười đọc bỏ cuộc sau 2 phútTách thành nhiều type alias, đặt tên rõ
Chain infer dàiKhó debug, hover không hiệnRefactor sang helper type ngắn hơn
tsc mỗi check >100msIDE lag, autocomplete chậmDùng --extendedDiagnostics để tìm thủ phạm
Hover hiển thị type quá dàiDeveloper 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ặtTách module, dùng interface thay vì type khi có thể
JSDoc giải thích còn dễ hơn typeType không còn đóng vai trò "tài liệu"Đổi sang JSDoc + type đơn giản
🧠 Mental model — "phép màu nhỏ" hay "phép thuật đen"

Type chỉ nên giúp bạn tránh bugtự 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:

Đo performance tsc
# 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.

DeepPartial — viết & trace
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;
// }
💡 Trick — hover trong VS Code

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:

Built-in = mapped + conditional
// 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>>number
  • MyAwaited<Promise<Promise<string>>>string
  • MyAwaited<Promise<Promise<Promise<boolean>>>>boolean
  • MyAwaited<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ắt Param rồi đệ quy phần Rest.
  • Param ở cuối (không có / sau): chỉ bắt Param.
  • Không có :: trả {}.

Đây là pattern dùng trong tRPCNext.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

Q1

Cho type X = T extends string ? 1 : 0. Khi T = 'a' | 42, X là gì?

Xem đáp án
✓ Đá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.

Q2

Làm sao tắt distributive conditional?

Xem đáp án
✓ Đá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ử.

Q3

infer dùng được ở đâu? Có dùng được ngoài conditional không?

Xem đáp án
✓ Đá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".

Q4

Mapped type mặc định có giữ readonly? của source không?

Xem đáp án
✓ Đáp án

Có (homomorphic). Mặc định mapped type giữ nguyên modifier readonly? của type gốc. Muốn thêm tường minh: +readonly, +?. Muốn bỏ: -readonly, -?.

Q5

Cho T = { a: 1; b: 2 }. keyof T là gì?

Xem đáp án
✓ Đá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.

Q6

Template literal `/api/${string}` match string nào?

Xem đáp án
✓ Đá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.

Q7

Recursive type của TS có giới hạn không? Bao nhiêu tầng?

Xem đáp án
✓ Đá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, +/-?key remap bằng as.
  • 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, Pick chính là ví dụ của mapped + conditional ở chương này.
  • Chương 4 (Objects & Class hierarchy)keyof và indexed access trên class instance, cách TS suy ra type từ this.