← TypeScript Thực Chiến

Bài 9 · Nâng cao · 24 phút

Utility types & tự xây helper

Biên soạn bởi Nguyễn Anh Tuấn

Bộ utility type có sẵn (Partial, Required, Pick, Omit, Record, ReturnType, Awaited…) và quan trọng hơn: TỰ DỰNG LẠI chúng từ mapped + conditional type để thấy chúng chỉ là kiểu mình cũng viết được. Nhìn under the hood thư viện kiểu chuẩn.

TypeScript kèm sẵn một bộ utility type - các kiểu biến đổi một kiểu khác, dùng được ngay không cần khai báo. Vài cái hay gặp nhất:

utility-co-san.ts

interface Meo { ten: string; tuoi: number; }

type A = Partial<Meo>;            // { ten?: string; tuoi?: number }
type B = Pick<Meo, "ten">;        // { ten: string }
type C = Omit<Meo, "ten">;        // { tuoi: number }
type D = Record<"a" | "b", number>;  // { a: number; b: number }
type E = ReturnType<() => boolean>;  // boolean
type F = NonNullable<string | null>; // string
  • Partial/Required: thêm hoặc bớt ? cho mọi field; Readonly: thêm readonly.
  • Pick giữ các khoá liệt kê; Omit loại các khoá liệt kê.
  • Record dựng object theo tập khoá; ReturnType/Parameters/Awaited bóc kiểu từ hàm/Promise.

Điều quan trọng nhất của bài này: các utility đó không có gì ma thuật - chúng chỉ là mapped type mà mèo con cũng viết được. Partial và Readonly chỉ là mapped type thêm modifier:

dung-lai-partial.ts

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

// MyPartial<Meo> giong HET Partial<Meo>
// MyReadonly<Meo> giong HET Readonly<Meo>
// (gan qua lai hai chieu khong loi -> hai kieu bang nhau)

Đã kiểm thật

Mình đã chạy tsc để chắc: gán qua lại hai chiều giữa MyPartial<Meo>Partial<Meo> không hề báo lỗi - tức chúng là cùng một kiểu. Bản "có sẵn" chỉ tiện hơn, không khác về bản chất.

Pick là mapped type chỉ duyệt các khoá được chọn; Omit thì pick những khoá không nằm trong danh sách loại, nhờ Exclude (vốn là một conditional):

dung-lai-pick-omit.ts

type MyPick<T, K extends keyof T> = { [P in K]: T[P] };

// Exclude<A, B> = A extends B ? never : A  (loai cac thanh vien khop B)
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;

// MyPick<Meo, "ten">  = { ten: string }   = Pick<Meo, "ten">
// MyOmit<Meo, "ten">  = { tuoi: number }  = Omit<Meo, "ten">
  • MyPick duyệt [P in K] - chỉ các khoá trong K, nên giữ đúng phần đó.
  • Exclude là conditional loại bỏ các thành viên union khớp điều kiện.
  • MyOmit = pick những khoá còn lại sau khi Exclude bỏ K khỏi keyof T.

Hai mảnh cuối: Record là mapped type sinh object theo tập khoá, còn NonNullable là một conditional loại null/undefined - đúng cái ta đã dựng ở bài conditional:

dung-lai-record-nonnullable.ts

type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyNonNullable<T> = T extends null | undefined ? never : T;

// MyRecord<"a" | "b", number>  = { a: number; b: number }
// MyNonNullable<string | null>  = string

Bức tranh lớn

Cả thư viện kiểu chuẩn của TypeScript được dựng từ đúng ba mảnh mèo con đã học: mapped type, conditional type, và infer. Hiểu ba mảnh đó là đọc được mã nguồn của bất kỳ utility nào, và tự viết được khi cần.

Khép lại nhóm tầng kiểu: utility type không phải phép màu, chỉ là mapped, conditional và infer ghép lại. Mèo con giờ vừa dùng được đồ có sẵn, vừa hiểu chúng từ bên trong. Từ bài sau, ta rời tầng kiểu để về với chuyện "đời thực" của một dự án TypeScript.

Bài tiếp theo: tsconfig, strict & .d.ts

Bài sau là phần thực dụng: cấu hình tsconfig, chế độ strict và tệp khai báo .d.ts - cách gắn kiểu cho thư viện JavaScript chưa có kiểu, và những lựa chọn cấu hình đáng quan tâm.

Câu hỏi thường gặp

Không. Nhớ vài cái hay dùng (Partial, Pick, Omit, Record, ReturnType) là đủ cho phần lớn công việc; còn lại cứ tra khi cần. Quan trọng hơn việc thuộc lòng là HIỂU chúng được dựng thế nào, để khi cần một biến đổi chưa có sẵn thì tự viết được.

Partial<T> làm mọi field thành optional (thêm ?). Required<T> làm ngược lại: gỡ ? để mọi field thành bắt buộc. Cả hai chỉ là mapped type thêm hoặc bớt modifier ? như ở Bài 8.

Pick<T, K> GIỮ LẠI đúng các khoá liệt kê trong K. Omit<T, K> thì ngược lại: LOẠI BỎ các khoá trong K, giữ phần còn lại. Omit thực ra là Pick những khoá không nằm trong K (dùng Exclude bên trong).

Khi bạn cần một object có tập khoá biết trước và mọi giá trị cùng kiểu. Record<"do" | "xanh", number> cho ra { do: number; xanh: number }. Hợp cho bảng tra, cấu hình theo khoá cố định.

Khi không có cái sẵn nào khớp nhu cầu - vd một biến đổi kiểu riêng cho dự án. Lúc đó bạn ghép mapped, conditional và infer (Bài 7-8) lại. Phần lớn thời gian cứ dùng đồ có sẵn; tự viết là kỹ năng để dành cho ca khó.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Utility type nào làm MỌI field của một kiểu thành optional?

Bài tập về nhà

  1. 1

    Điểm danh utility

    Với một interface có 3 field, áp lần lượt Partial, Required, Readonly, Pick (một khoá), Omit (một khoá). Ghi lại shape kết quả của mỗi cái.

    ✅ Hoàn thành khi: Năm dòng: tên utility và shape kết quả tương ứng, đúng từng field.

  2. 2

    Dựng lại Partial

    Tự viết MyPartial<T> = { [K in keyof T]?: T[K] }. Kiểm nó cho ra y hệt Partial bằng cách gán qua lại hai chiều giữa MyPartial<T> và Partial<T>.

    ✅ Hoàn thành khi: Gán hai chiều không lỗi, chứng minh MyPartial giống hệt Partial có sẵn.

  3. 3

    Dựng lại Pick

    Viết MyPick<T, K extends keyof T> = { [P in K]: T[P] }. Áp lấy một khoá, so với Pick có sẵn.

    ✅ Hoàn thành khi: MyPick lấy đúng khoá yêu cầu và khớp Pick có sẵn (gán hai chiều không lỗi).

  4. 4

    Dựng lại Omit

    Viết MyOmit dùng MyPick và Exclude<keyof T, K> để giữ các khoá KHÔNG nằm trong K. So với Omit có sẵn.

    ✅ Hoàn thành khi: MyOmit loại đúng khoá cần bỏ; kèm một câu giải thích Exclude (một conditional) làm gì.

  5. 5

    Chọn đúng utility

    Cho 3 tình huống: cập nhật một phần object, tạo một form chỉ gồm vài field, ẩn một field nhạy cảm khi trả về. Chọn utility phù hợp cho từng cái.

    ✅ Hoàn thành khi: Ba lựa chọn kèm lý do: vd Partial cho cập nhật phần, Pick cho form, Omit cho ẩn field.

  6. 6

    Soi nguồn gốc

    Chọn 3 utility type bất kỳ. Với mỗi cái, nói nó được dựng chủ yếu từ mapped type, conditional type, hay infer.

    ✅ Hoàn thành khi: Ba utility kèm phân loại đúng (vd Partial = mapped, NonNullable = conditional, ReturnType = conditional + infer).