Chương 10 · TypeScript Generics & Utility Types

Generic và Utility Types — viết một lần, dùng cho mọi type

Generic biến function/class/type thành template tham số hoá theo type — như function nhận arg nhưng arg ở đây là type. Utility types là bộ generic built-in của TypeScript: biến đổi type sẵn có thành type mới (làm optional, lọc key, rút type từ function). Master 2 thứ này là bước nhảy từ "biết TS" sang "viết được API library".

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 9 (TS Foundations)
🎯 Mục tiêu chương
  • Viết generic function với 1, 2, nhiều type parameter, hiểu cơ chế type inference.
  • Dùng extends để constraint type parameter; kết hợp với keyof.
  • Đặt default type parameter để API gọn nhưng vẫn linh hoạt.
  • Định nghĩa generic class, interface, type alias.
  • Master 11 utility types: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited.
  • Bắt đầu tự viết utility (Nullable, DeepPartial stub) — cầu nối sang chương 11.

1. Generic function — type là tham số

Bắt đầu bằng một function "không generic" để thấy vấn đề. Muốn viết hàm identity trả chính giá trị truyền vào, làm việc với mọi type:

Vấn đề trước khi có generic
// Cách 1 — quá hẹp, mỗi type 1 hàm
function identityNumber(x: number): number { return x; }
function identityString(x: string): string { return x; }

// Cách 2 — any, mất type
function identity(x: any): any { return x; }
const r = identity(5);   // r: any — không gợi ý gì cả
r.toUpperCase();         // 💥 runtime error, TS không catch

Giải pháp: thêm type parameter T trong cặp <...>. T như một biến type, được "điền" khi gọi hàm:

identity<T>
function identity<T>(x: T): T {
  return x;
}

// Cách 1 — gọi explicit, chỉ định T
const a = identity<number>(5);     // a: number
const b = identity<string>("hi"); // b: string

// Cách 2 — để TS infer (thường dùng)
const c = identity(5);            // c: number — T inferred = number
const d = identity("hi");         // d: string
const e = identity([1, 2, 3]);    // e: number[]
🧠 Mental model — generic ≈ function ở "tầng type"

Hình dung TypeScript có 2 tầng: tầng value (giá trị runtime — chạy bởi JS) và tầng type (biến mất khi compile). Function thường nhận value và trả value. Generic function còn nhận type và trả type.

identity<T>(x: T): T đọc là: "cho tôi type T, tôi trả function nhận T trả T". Khi bạn viết identity(5), compiler chạy thuật toán inference: "x là number, mà x: T → T = number → hàm trả number".

2. Nhiều type parameter

Một generic có thể nhận nhiều type parameter, đặt cách nhau bằng dấu phẩy trong <...>. Quy ước đặt tên: T, U, V cho generic chung; K cho key, V cho value, E cho element, R cho return.

pair<A, B>
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p1 = pair(1, "x");        // p1: [number, string]
const p2 = pair(true, [1, 2]);  // p2: [boolean, number[]]

// Ví dụ thực tế — zip 2 array
function zip<A, B>(as: A[], bs: B[]): [A, B][] {
  const n = Math.min(as.length, bs.length);
  const out: [A, B][] = [];
  for (let i = 0; i < n; i++) out.push([as[i], bs[i]]);
  return out;
}

const z = zip([1, 2], ["a", "b"]);  // z: [number, string][]

Một ví dụ rất hay trong thực tế — map tự viết, biến mảng T[] thành mảng U[]:

function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  const out: U[] = [];
  for (const x of arr) out.push(fn(x));
  return out;
}

const lengths = map(["hi", "world"], s => s.length);
// lengths: number[]   ← TS infer T=string, U=number

3. Constraint — bó hẹp type parameter với extends

Bình thường T là "bất cứ type nào". Nhưng đôi khi ta cần đảm bảo T có vài property. Dùng T extends SomeType:

Constraint cơ bản
// Không constraint — sai vì không phải mọi T có .length
function len<T>(x: T): number {
  return x.length;  // ❌ Property 'length' does not exist on type 'T'
}

// Có constraint — T phải có .length
function len<T extends { length: number }>(x: T): number {
  return x.length;
}

len("hello");          // OK — string có length
len([1, 2, 3]);        // OK — array có length
len({ length: 10, x: 1 }); // OK — object có length
len(42);               // ❌ number không có length

Combo cực mạnh: K extends keyof T. Đảm bảo keyreal key của object — không chấp nhận string ngẫu nhiên. Đây là kỹ thuật chủ lực để viết "object accessor" type-safe:

getProp<T, K extends keyof T>
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "An", active: true };

const id   = getProp(user, "id");     // id: number
const name = getProp(user, "name");   // name: string
const act  = getProp(user, "active"); // act: boolean

getProp(user, "email");
// ❌ Argument of type '"email"' is not assignable to parameter of type 'keyof typeof user'
💡 Vì sao thêm K (không chỉ T)?

Nếu viết function getProp<T>(obj: T, key: keyof T): T[keyof T], return type là union của mọi value type — bạn không biết cụ thể nào. Tách K ra giúp TS "ghi nhớ" đúng key user đã truyền, từ đó suy ra T[K] chính xác.

4. Default type parameter

Giống default value cho function param, type parameter có thể có default. Cú pháp T = SomeType. Khi gọi không spec, TS lấy default:

Default type parameter
function createArray<T = string>(): T[] {
  return [];
}

const a = createArray();            // a: string[]   (default)
const b = createArray<number>();    // b: number[]

// Combo: default + constraint
interface ApiResponse<T extends object = {}> {
  status: number;
  data: T;
}

const r1: ApiResponse = { status: 200, data: {} };
const r2: ApiResponse<{ id: number }> = { status: 200, data: { id: 1 } };

Default rất hay khi viết library — user không spec gì vẫn dùng được, nhưng vẫn override khi cần type cụ thể.

5. Generic class

Class cũng nhận type parameter. Type parameter sống ở cấp class — mọi property/method đều dùng được:

Generic Stack
class Stack<T> {
  private items: T[] = [];

  push(x: T): void {
    this.items.push(x);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
const top = numbers.pop();  // top: number | undefined

const strs = new Stack<string>();
strs.push("hello");
strs.push(42);  // ❌ number không gán vào string
🧠 Mỗi instance "đóng băng" 1 T

Mỗi instance Stack có một T cụ thể: Stack<number>Stack<string> là 2 type khác nhau. Khi compile, generic biến mất, chỉ còn class Stack JS thường — type là công cụ tĩnh.

6. Generic interface và type alias

Interface và type alias cũng nhận type parameter. Đây là cách định nghĩa container type tái sử dụng được:

Box<T> — container đơn giản
// Cách 1 — interface
interface Box<T> {
  value: T;
}

// Cách 2 — type alias (tương đương)
type Box2<T> = { value: T };

const b1: Box<number> = { value: 42 };
const b2: Box<string> = { value: "hi" };

// Generic interface trong API — pattern phổ biến
interface Result<T, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
}

const success: Result<number> = { ok: true, data: 42 };
const fail: Result<number, string> = { ok: false, error: "timeout" };

Function signature cũng có thể là generic interface — cách định nghĩa "callable generic":

// Interface mô tả function generic
interface Mapper {
  <T, U>(arr: T[], fn: (x: T) => U): U[];
}

const myMap: Mapper = (arr, fn) => arr.map(fn);
myMap([1, 2], n => n * 2);  // number[]

7. Utility types: Partial, Required, Readonly

Từ chương này về sau, ta dùng utility types — sẵn có trong TS. Đây là generic được viết sẵn để biến đổi type. Bộ 3 đầu tiên: bật/tắt optional và readonly.

UtilityTác dụngTương đương
Partial<T>Mọi property thành optional{ [K in keyof T]?: T[K] }
Required<T>Mọi property thành bắt buộc{ [K in keyof T]-?: T[K] }
Readonly<T>Mọi property thành readonly{ readonly [K in keyof T]: T[K] }
Partial — pattern update DTO
interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
}

// updateUser nhận subset of User — chỉ field nào muốn đổi
function updateUser(id: number, patch: Partial<User>): User {
  // ... merge với user hiện tại
  return { id, name: "x", email: "x", active: true, ...patch };
}

updateUser(1, { name: "An" });               // OK
updateUser(1, { email: "a@b", active: false }); // OK
updateUser(1, {});                              // OK (rỗng cũng được)
🔥 Partial chỉ "shallow" — không đi sâu

Partial<T> chỉ làm optional ở tầng 1. Nếu property là object lồng, object đó vẫn nguyên cấu trúc, không tự optional bên trong.

interface Config {
  ui: { theme: string; lang: string };
  api: { url: string };
}

type P = Partial<Config>;
// = { ui?: { theme: string; lang: string }; api?: { url: string } }

const p: P = { ui: { theme: "dark" } };  // ❌ thiếu lang

Cần "đệ quy" → tự viết DeepPartial<T> bằng conditional/mapped type — chương 11 sẽ làm.

Required — ngược lại Partial
interface PartialConfig {
  theme?: string;
  lang?: string;
  debug?: boolean;
}

type FullConfig = Required<PartialConfig>;
// = { theme: string; lang: string; debug: boolean }   ← bỏ tất cả ?

function applyConfig(c: FullConfig) {
  // c.theme đảm bảo có, không phải undefined
}

Readonly<T> hữu ích khi muốn nhấn mạnh "không được mutate" — vd state immutable, config app.

interface Point { x: number; y: number; }

const p: Readonly<Point> = { x: 1, y: 2 };
p.x = 5;  // ❌ Cannot assign to 'x' because it is a read-only property

8. Utility types: PickOmit

Hai utility "ngược nhau" để tạo subtype: chọn hay loại một số key.

UtilityTác dụngVí dụ
Pick<T, K>Lấy chỉ key trong KPick<User, "id" | "name">
Omit<T, K>Bỏ key trong KOmit<User, "password">
Pick / Omit thực tế
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Trả API: không lộ password
type UserResponse = Omit<User, "password">;
// = { id: number; name: string; email: string; createdAt: Date }

// Item trong list — chỉ cần id + name
type UserListItem = Pick<User, "id" | "name">;
// = { id: number; name: string }

// Form đăng nhập — chỉ email + password
type LoginForm = Pick<User, "email" | "password">;
💡 Mối quan hệ Pick & Omit

Chúng "lưỡng tính" — biểu diễn qua nhau:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// "Omit K" = "Pick mọi key của T trừ K"

Đây thực sự là cách Omit được định nghĩa trong lib.es5.d.ts của TS.

9. Utility type: Record<K, V>

Tạo object type với key trong union K, value type V. Cú pháp gọn hơn index signature.

Record — dictionary với key cố định
// Object có chính xác 3 key Role → number (count user)
type Role = "admin" | "user" | "guest";

type RoleCount = Record<Role, number>;
// = { admin: number; user: number; guest: number }

const counts: RoleCount = {
  admin: 2,
  user: 100,
  guest: 42,
};

counts.user;   // number
counts.other;  // ❌ Property 'other' does not exist

// String key bất kỳ — tương đương index signature
type Dict<V> = Record<string, V>;
// = { [key: string]: V }

const ages: Dict<number> = { an: 22, binh: 23 };
🧠 Record<K, V> vs index signature

Record<string, number>{ [key: string]: number } tương đương. Record chỉ là "sugar" cho mapped type:

type Record<K extends keyof any, V> = {
  [P in K]: V;
};

Khác biệt thực dụng: Record đẹp hơn khi K là union cụ thể (vd "a"|"b"|"c") — nó "ghi" đủ key cho IntelliSense. Index signature đẹp hơn khi key thực sự "bất kỳ".

10. Utility types lọc union: Exclude, Extract, NonNullable

Bộ ba này làm việc với union. Hữu ích cho narrowing type, lọc literal, bỏ null/undefined.

UtilityÝ nghĩaVí dụ
Exclude<T, U>Loại member của T nằm trong UExclude<"a"|"b"|"c", "a"> = "b"|"c"
Extract<T, U>Giữ lại member của T nằm trong UExtract<"a"|"b"|1, string> = "a"|"b"
NonNullable<T>Loại null & undefinedNonNullable<string|null> = string
Filter union
type Status = "pending" | "success" | "error" | "cancelled";

// Bỏ "cancelled"
type ActiveStatus = Exclude<Status, "cancelled">;
// = "pending" | "success" | "error"

// Chỉ giữ "success" và "error"
type DoneStatus = Extract<Status, "success" | "error">;
// = "success" | "error"

// Bỏ null/undefined
type MaybeName = string | null | undefined;
type Name = NonNullable<MaybeName>;  // = string

// Áp dụng trong function — narrow sau check
function trim(s: string | null): string {
  if (s === null) return "";
  const v: NonNullable<typeof s> = s;  // v: string
  return v.trim();
}

11. Utility types về function: ReturnType, Parameters, Awaited

Bộ 3 này "rút" thông tin từ type của function/promise. Cực kỳ hữu ích khi bạn không muốn viết tay lại type mà function khác đã định nghĩa.

UtilityInputOutput
ReturnType<F>function typeType của giá trị return
Parameters<F>function typeTuple type của param
Awaited<P>Promise typeType bên trong Promise (đệ quy)
ReturnType và Parameters
function createUser(name: string, age: number) {
  return { id: Date.now(), name, age, active: true };
}

// Lấy return type — không cần định nghĩa lại
type User = ReturnType<typeof createUser>;
// = { id: number; name: string; age: number; active: boolean }

// Lấy tuple param
type Args = Parameters<typeof createUser>;
// = [name: string, age: number]

// Lấy param đầu
type FirstArg = Parameters<typeof createUser>[0];  // = string
💡 Vì sao phải có typeof trước tên function?

createUservalue (function thật). ReturnType cần một type. Toán tử typeof ở vị trí type "lấy type của value đó". Tương tự pattern hay gặp:

const config = { theme: "dark", lang: "vi" };
type Config = typeof config;  // = { theme: string; lang: string }

Đây là typeof ở "tầng type" — không phải typeof runtime trả string. Cùng từ khoá, 2 nghĩa khác nhau tuỳ ngữ cảnh.

Awaited — unwrap Promise
async function fetchUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: "An" };
}

// ReturnType — chỉ lấy Promise<...>, chưa unwrap
type R = ReturnType<typeof fetchUser>;
// R = Promise<{ id: number; name: string }>

// Awaited — lấy giá trị bên trong Promise
type User = Awaited<R>;
// User = { id: number; name: string }

// Combo phổ biến
type User2 = Awaited<ReturnType<typeof fetchUser>>;

// Awaited đệ quy — unwrap Promise lồng
type A = Awaited<Promise<Promise<number>>>;  // = number

12. Tự viết utility — bước đầu

Bạn không phải lúc nào cũng phải dùng utility built-in — tự viết được khi cần. Ở chương 11 (Advanced Types) ta học đầy đủ conditional và mapped type. Chương này chỉ "nếm thử":

Utility tự viết
// 1. Nullable — thêm null vào union
type Nullable<T> = T | null;

const name: Nullable<string> = null;       // OK
const name2: Nullable<string> = "hello";   // OK

// 2. Optional — thêm null + undefined
type Maybe<T> = T | null | undefined;

// 3. ValueOf — tương tự keyof nhưng cho value type
type ValueOf<T> = T[keyof T];

interface Color { red: 1; green: 2; blue: 3; }
type ColorValue = ValueOf<Color>;  // = 1 | 2 | 3

// 4. DeepPartial — phiên bản đệ quy của Partial
//    (đầy đủ ở chương 11 — đây là intro)
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? DeepPartial<T[K]>
    : T[K];
};

interface Config {
  ui: { theme: string; lang: string };
  api: { url: string; timeout: number };
}

const p: DeepPartial<Config> = {
  ui: { theme: "dark" },   // thiếu lang — OK vì đã đệ quy
};
💡 Đặt utility ở đâu?

Quy ước phổ biến: tạo file src/types/utils.ts (hoặc shared/types.ts), export type những utility tự viết. Một số project lớn dùng package type-fest (Sindre Sorhus) — bộ ~150 utility chất lượng cao, đỡ tự viết lại.

13. Bảng tổng kết utility types

Một bảng để tra cứu nhanh sau khi đã đọc xong từng phần:

UtilityInput → OutputDùng khi
Partial<T>Mọi property → optionalUpdate DTO, patch object
Required<T>Mọi property → bắt buộcSau khi merge default, đảm bảo đủ field
Readonly<T>Mọi property → readonlyState immutable, config
Pick<T, K>Lấy key trong KList item, form subset
Omit<T, K>Bỏ key trong KHide password, ẩn id khi tạo mới
Record<K, V>Object {K → V}Dictionary key cố định
Exclude<T, U>Bỏ member union T trong UNarrow status, lọc literal
Extract<T, U>Giữ member T trong ULọc loại event, giữ string trong mixed
NonNullable<T>Bỏ null + undefinedSau narrowing, force non-null
ReturnType<F>Lấy return của FReuse type của function khác
Parameters<F>Lấy tuple param của FWrap/decorate function
Awaited<P>Unwrap Promise (đệ quy)Lấy giá trị async return

Bài tập

Bài 1 — Generic map function

Viết function map<T, U>(arr: T[], fn: (x: T) => U): U[] — không dùng Array.prototype.map. Test với:

  • map([1, 2, 3], n => n.toString())["1", "2", "3"] kiểu string[]
  • map(["a", "bb", "ccc"], s => s.length)[1, 2, 3] kiểu number[]
Đáp án
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  const out: U[] = [];
  for (const x of arr) {
    out.push(fn(x));
  }
  return out;
}

const strs = map([1, 2, 3], n => n.toString());
// strs: string[]   ← TS infer T=number, U=string

const lens = map(["a", "bb", "ccc"], s => s.length);
// lens: number[]

Điểm chính: 2 type parameter — input element type và output element type — TS tự suy ra cả 2 từ argument.

Bài 2 — groupBy

Viết function groupBy<T, K extends string | number>(arr: T[], keyFn: (x: T) => K): Record<K, T[]> — gom mảng thành object theo key. Ví dụ:

const users = [
  { name: "An", role: "admin" },
  { name: "Binh", role: "user" },
  { name: "Cuong", role: "admin" },
];

groupBy(users, u => u.role);
// = { admin: [An, Cuong], user: [Binh] }
Đáp án
function groupBy<T, K extends string | number>(
  arr: T[],
  keyFn: (x: T) => K
): Record<K, T[]> {
  const out = {} as Record<K, T[]>;
  for (const x of arr) {
    const k = keyFn(x);
    (out[k] ??= []).push(x);
  }
  return out;
}

Constraint K extends string | number đảm bảo có thể dùng làm key object. Pattern (out[k] ??= []).push(x) dùng logical-nullish-assignment (ES2021): nếu key chưa có → tạo array rỗng → push.

Bài 3 — Type-safe event emitter

Cho:

type EventMap = {
  click: MouseEvent;
  submit: SubmitEvent;
  keypress: KeyboardEvent;
};

Viết function on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void): void. Yêu cầu:

  • on("click", e => e.clientX) hợp lệ (e là MouseEvent)
  • on("submit", e => e.preventDefault()) hợp lệ
  • on("click", e => e.key) ❌ (key chỉ có ở KeyboardEvent)
  • on("unknown", ...) ❌ (không có trong EventMap)
Đáp án
type EventMap = {
  click: MouseEvent;
  submit: SubmitEvent;
  keypress: KeyboardEvent;
};

function on<K extends keyof EventMap>(
  event: K,
  handler: (e: EventMap[K]) => void
): void {
  document.addEventListener(event, handler as EventListener);
}

on("click", e => console.log(e.clientX));     // OK
on("submit", e => e.preventDefault());          // OK
on("click", e => e.key);                       // ❌ key không có trên MouseEvent
on("unknown", () => {});                       // ❌ "unknown" không phải keyof EventMap

Đây là pattern core của type-safe event system — react synthetic events, Node EventEmitter typed, RxJS. Cả 2 lỗi compile-time, không cần runtime check.

Bài 4 — User DTOs với Pick/Omit

Cho interface User 10 field:

interface User {
  id: number;
  email: string;
  password: string;
  name: string;
  avatar: string;
  bio: string;
  role: "admin" | "user";
  active: boolean;
  createdAt: Date;
  updatedAt: Date;
}

Hãy tạo 4 type sau bằng Pick/Omit/Partial:

  • CreateUserDto — bỏ id, createdAt, updatedAt (server tự sinh)
  • UpdateUserDto — như CreateUserDto nhưng mọi field optional
  • UserResponse — bỏ password
  • UserListItem — chỉ id, name, avatar
Đáp án
// 1. Tạo mới — bỏ field server-generated
type CreateUserDto = Omit<User, "id" | "createdAt" | "updatedAt">;

// 2. Update — như tạo mới + mọi field optional
type UpdateUserDto = Partial<CreateUserDto>;
// Hoặc: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>

// 3. Response — không lộ password
type UserResponse = Omit<User, "password">;

// 4. List — chỉ thông tin để hiển thị
type UserListItem = Pick<User, "id" | "name" | "avatar">;

// Test
const dto: CreateUserDto = {
  email: "a@b.com",
  password: "x",
  name: "An",
  avatar: "",
  bio: "",
  role: "user",
  active: true,
};

const patch: UpdateUserDto = { name: "An mới" };  // OK chỉ 1 field

Pattern này là chuẩn cho REST API. Một interface User duy nhất sinh ra 4 type khác nhau cho 4 ngữ cảnh — không lặp code, đổi User → tất cả tự đồng bộ.

Bài 5 — OptionalKeys<T>

Viết utility type OptionalKeys<T> trả về union của các key trong T mà property là optional (có ?). Ví dụ:

interface User {
  id: number;
  name: string;
  email?: string;
  bio?: string;
}

type Opt = OptionalKeys<User>;  // = "email" | "bio"

Gợi ý: dùng conditional type. So sánh {} với Pick<T, K> — nếu một key K là optional, {} có thể assignable vào Pick<T, K>.

Đáp án
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

// Đối xứng — key bắt buộc
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

// Test
interface User { id: number; name: string; email?: string; bio?: string; }
type Opt = OptionalKeys<User>;   // "email" | "bio"
type Req = RequiredKeys<User>;   // "id" | "name"

Cơ chế: với mỗi K, kiểm tra {} extends Pick<T, K>:

  • Nếu K optional → Pick<T, K>{ K?: ... }{} assignable → kết quả K.
  • Nếu K bắt buộc → Pick{ K: ... }{} không assignable → kết quả never.

Cuối cùng [keyof T] "indexed access" lấy union các value — never tự loại bỏ. Đây là pattern conditional + mapped type "kinh điển" — chương 11 sẽ phân tích kỹ.

Quiz

Q1

Với function f<T>(x: T): T { return x; }, khi gọi f(5), T là gì?

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

T = number. TS dùng type inference — nhìn vào argument 5 có type number, đối chiếu với param x: T → suy ra T = number. Không cần viết f<number>(5) tường minh (vẫn đúng nhưng dư).

Q2

Partial<T> có đệ quy không (làm optional ở nested object)?

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

Không. Partial chỉ làm optional ở tầng 1. Nested object vẫn nguyên cấu trúc. Muốn đệ quy phải tự viết DeepPartial bằng conditional type (xem mục 12 và chương 11).

Q3

Pick<T, K>Omit<T, K> "đối xứng" thế nào?

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

Omit<T, K> = Pick<T, Exclude<keyof T, K>>. Tức là "Omit K" tương đương "Pick tất cả key của T trừ K". Đây chính là cách Omit được định nghĩa trong lib.es5.d.ts của TypeScript.

Q4

Khác nhau giữa Record<string, number>{ [key: string]: number }?

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

Tương đương về mặt type. Record là "sugar" cho mapped type: Record<K, V> = { [P in K]: V }. Khác biệt thực dụng: dùng Record khi K là union cụ thể (vd "a"|"b"|"c") — IntelliSense gợi ý đủ key. Index signature thuận tự nhiên hơn khi key thực sự "bất kỳ string".

Q5

Vì sao viết ReturnType<typeof foo> mà không phải ReturnType<foo>?

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

foovalue (function thật khi runtime), còn ReturnType nhận type. Toán tử typeoftầng type "chuyển" value thành type tương ứng — không phải typeof runtime trả string. Cùng từ khoá, 2 nghĩa khác nhau tuỳ vị trí (type position vs value position).

Q6

Generic class có default cho type parameter không?

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

. Cú pháp giống function: class Container<T extends object = {}>. Khi tạo instance không spec type, TS lấy default. Pattern này phổ biến trong React (vd useState<T = undefined>), state library.

Q7

Required<T> có loại bỏ ? kể cả những field vốn đã optional?

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

. Required<T> ép tất cả property thành bắt buộc, kể cả original ?. Định nghĩa: { [K in keyof T]-?: T[K] } — dấu -? là "remove optional modifier". Tương tự Readonly dùng readonly và có "mặt nghịch" là -readonly.

Tổng kết

Sau chương 10, bạn nên đã thành thạo:

  • Generic function: 1, 2, nhiều type parameter; hiểu inference vs spec tường minh.
  • Constraint: T extends ..., đặc biệt combo K extends keyof T.
  • Default type parameter: T = string, gọn API mà vẫn linh hoạt.
  • Generic classgeneric interface/type: container, callable signature.
  • 11 utility types built-in: bốn nhóm — modifier (Partial/Required/Readonly), select (Pick/Omit/Record), filter union (Exclude/Extract/NonNullable), từ function (ReturnType/Parameters/Awaited).
  • Cảm giác đầu tiên với conditional & mapped type tự viết — sẽ "deep dive" ở chương 11.

Kết nối

  • Chương 11 (TS Advanced Types) — conditional type, mapped type, template literal type, infer. Đây là engine đằng sau mọi utility hôm nay; sau chương 11 bạn sẽ viết được mọi utility tự thân.
  • Chương 12 (TS Practical) — áp dụng generic + utility vào project React + Node thật. tsconfig khắt khe, declaration file, integration với Express/Fastify.
  • Dart Chương 8 (Generic) — đối chiếu generic Dart (cũng có extends) vs TS (cộng thêm conditional). Dart không có utility "biến đổi type" tương đương Pick/Omit — type system của TS mạnh hơn ở mặt này.
  • Python typing (pillar tương lai) — TypeVar, Generic[T], TypedDict — tương ứng generic/interface, nhưng yếu hơn về utility.