Trung bình JavaScript

TypeScript Utility Types: Pick, Omit Với Partial Xài Sao Cho Đúng

Đừng copy-paste interface cho từng biến thể nhỏ nhỏ nữa. Pick, Omit với Partial giúp mình dẫn xuất type mới từ một nguồn duy nhất - đây là lúc nào nên xài cái nào.

22 Th07, 2026 4 phút 339 Lượt xem 5 Khối code
Sơ đồ
graph TD A["User (source of truth)"] --> B["Partial<User><br/>all fields optional"] A --> C["Pick<User, 'id' | 'name'><br/>only listed fields"] A --> D["Omit<User, 'passwordHash'><br/>everything except listed"] C --> E["Partial<Pick<User,...>><br/>subset, all optional"]

TypeScript Utility Types: Pick, Omit Với Partial Xài Sao Cho Đúng

API nào cũng cần một hình dạng hơi khác của cùng một dữ liệu: User đầy đủ, User an toàn để public (bỏ password hash), một form chỉ update đúng 2 field. Copy-paste interface cho từng trường hợp là kiểu gì cũng lệch nhau ngay khi có chỗ update. Utility type có sẵn của TypeScript giải quyết chuyện này bằng cách dẫn xuất type mới từ một nguồn sự thật duy nhất.

Vấn đề

interface User {
    id: string
    name: string
    email: string
    passwordHash: string
    createdAt: Date
}

// Bị trùng lặp, theo thời gian sẽ lệch dần so với User
interface PublicUser {
    id: string
    name: string
    email: string
    createdAt: Date
}

Mỗi lần User đổi là phải nhớ update PublicUser theo. Mà thường là quên.

Partial<T>: Biến Mọi Field Thành Optional

Hợp nhất cho payload update, khi caller chỉ gửi những field nào thay đổi thôi.

function updateUser(id: string, changes: Partial<User>) {
    // changes có thể là { name: 'New Name' } hoặc { email: '...' }
    return db.users.update(id, changes)
}

Pick<T, K>: Chọn Ra Một Tập Con Field

Xài khi chỉ cần vài field từ type lớn hơn - card preview, option dropdown.

type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }

Omit<T, K>: Loại Bỏ Field Cụ Thể

Ngược lại với Pick - giữ mọi thứ trừ mấy cái mình liệt kê ra. Hợp để lọc field nhạy cảm trước khi trả response.

type PublicUser = Omit<User, 'passwordHash'>
// { id: string; name: string; email: string; createdAt: Date }

Giờ PublicUser tự động bám theo User luôn. Thêm field vào User là nó tự xuất hiện trong PublicUser, khỏi đụng vào dòng này.

Kết Hợp Chúng Lại

Utility type ghép được với nhau. Một "form đổi tên" chỉ cho sửa mỗi tên:

type RenameForm = Partial<Pick<User, 'name'>>
// { name?: string }

Mấy Type Này Dẫn Xuất Từ Một Nguồn Ra Sao

graph TD A["User (nguồn sự thật)"] --> B["Partial&lt;User&gt;<br/>mọi field optional"] A --> C["Pick&lt;User, 'id' | 'name'&gt;<br/>chỉ field được liệt kê"] A --> D["Omit&lt;User, 'passwordHash'&gt;<br/>mọi thứ trừ field liệt kê"] C --> E["Partial&lt;Pick&lt;User,...&gt;&gt;<br/>tập con, đều optional"]

Khi Nào Xài Cái Nào

  • Partial<T>: payload PATCH/update, object config optional, form state trước khi submit.
  • Pick<T, K>: preview, option dropdown, chỗ nào cần tập con nhỏ gọn, tường minh.
  • Omit<T, K>: lọc field nhạy cảm hay nội bộ (password, ID nội bộ) trong khi vẫn tự động đồng bộ mọi thứ còn lại.

Mấy Cái Bẫy Hay Gặp

  • Omit với key đã đổi tên/xóa vẫn im lặng chạy qua - TypeScript không báo lỗi nếu mình omit một key đã bị xóa khỏi type gốc rồi, nên gõ sai key trong danh sách dễ bị bỏ sót lắm. Nhớ soát lại từng key so với type nguồn.
  • Partial che mất field thật sự bắt buộc - hàm update dùng Partial<User> không phân biệt được "caller quên id" với "caller không muốn đổi id". Giữ định danh bắt buộc (như id) ở ngoài Partial: { id: string } & Partial<Omit<User, 'id'>>.
  • Xài Record<string, unknown> thay vì Pick/Omit là bỏ luôn type safety - utility type giữ được liên kết với hình dạng gốc, còn Record lỏng lẻo thì không.

Dẫn xuất, đừng lặp lại. Nếu hai type mô tả cùng một entity, một trong hai nên được build từ cái kia bằng Pick, Omit, hoặc Partial - đừng maintain tay.

Cùng chủ đề JavaScript