Tuyệt kỹ "Parameterized Partial" và xử lý Optional-Prop Defaults chuẩn Senior
Chào anh em! Nếu đã làm việc với TypeScript đủ lâu, chắc hẳn anh em đã quá quen mặt với utility type Partial<T>. Nó là một "phép thuật" tuyệt vời giúp biến mọi thuộc tính trong một interface từ bắt buộc thành tùy chọn (optional) chỉ trong một nốt nhạc.
Nhưng đời không như mơ, requirement của các dự án thực tế thì luôn thiên biến vạn hóa. Sẽ có những lúc anh em vò đầu bứt tai: "Tôi chỉ muốn làm optional MỘT VÀI thuộc tính thôi, còn những cái khác vẫn phải bắt buộc cơ!". Và khi các thuộc tính tùy chọn đó đi kèm với giá trị mặc định (Optional-prop defaults), làm sao để code vừa gọn gàng lại vừa không bị TypeScript "hành"?
Hôm nay, mình sẽ chia sẻ một kỹ thuật cực kỳ lợi hại: Parameterized Partial kết hợp với Optional-prop defaults. Đây là bộ đôi đã cứu rỗi mình khỏi "địa ngục" duplicate code trong rất nhiều dự án.
Câu chuyện người từng trải: Ác mộng mang tên "Duplicate Interfaces" Ngày xưa lúc mới "chuyển sinh" từ JS sang TS, mình từng maintain một hệ thống khá to. Khổ một nỗi, team cũ định nghĩa type rất cồng kềnh.
Để mô tả một đối tượng User, dự án có một đống file thế này:
- User: Chứa đầy đủ các trường ánh xạ từ Database.
- CreateUserDTO: Copy y hệt User, nhưng xóa trường id và đặt role, status thành optional (vì khi tạo mới, Database đã có default value là user và active).
- UpdateUserDTO: Dùng
Partial<User>.
Hệ quả là mỗi lần thêm một trường mới (ví dụ: phoneNumber) vào bảng users, mình phải hì hục đi cập nhật ở 3-4 interface khác nhau. Quên một cái là bug sinh ra ngay lập tức. Mọi thứ chỉ thực sự được giải quyết khi mình ngộ ra cách tự build các Utility Types tùy chỉnh.
1. Parameterized Partial: "Chỉ cho phép tùy chọn những thứ cần thiết"
TypeScript cung cấp sẵn Partial<T> (biến tất cả thành optional) và Omit<T, K> (loại bỏ một số trường). Vậy làm sao để tạo ra một type mà chỉ có một vài trường cụ thể (parameterized) được phép optional?
Chúng ta sẽ kết hợp các core utility types lại với nhau để tạo ra một "vũ khí" mới. Cộng đồng thường gọi nó là PartialBy hoặc Optional.
Cú pháp định nghĩa:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
Giải phẫu đoạn code trên:
- Pick<T, K>: Lọc ra chính xác các trường K mà bạn muốn làm optional.
- Partial<Pick<T, K>>: Phủ phép Partial lên để biến đám vừa lọc ra thành optional thực sự.
- Omit<T, K>: Lấy TẤT CẢ các trường còn lại của T (những trường này không nằm trong danh sách K nên vẫn giữ nguyên tính bắt buộc).
- Giao (Intersection) bằng dấu &: Gộp hai nửa trên lại thành một khối hoàn chỉnh.
Ví dụ thực chiến:
interface User {
id: string;
username: string;
email: string;
role: string; // Mặc định là 'user'
isActive: boolean; // Mặc định là true
}
// Khi tạo user mới, id, role và isActive là không bắt buộc
type CreateUserPayload = PartialBy<User, 'id' | 'role' | 'isActive'>;
/*
Lúc này, TypeScript sẽ ngầm hiểu CreateUserPayload là:
{
username: string; // BẮT BUỘC
email: string; // BẮT BUỘC
id?: string; // Tùy chọn
role?: string; // Tùy chọn
isActive?: boolean; // Tùy chọn
}
*/
Chỉ với một dòng định nghĩa, anh em đã tái sử dụng lại User interface một cách hoàn hảo mà không cần phải đẻ thêm interface rác.
2. Optional-Prop Defaults: Nghệ thuật xử lý giá trị mặc định
Khi đã có type xịn sò ở bước 1, vấn đề tiếp theo thường gặp ở các hàm cấu hình hoặc thiết kế React Component là: Làm sao để gán default value cho các props optional đó một cách "clean" nhất?
Cách "cũ rích" (và dễ sinh lỗi type) mà nhiều người mới thường viết:
// ❌ KHÔNG NÊN
function createUser(payload: CreateUserPayload) {
// Phải gán tay thế này rất mệt mỏi
const role = payload.role ? payload.role : 'user';
const isActive = payload.isActive !== undefined ? payload.isActive : true;
}
Cách chuẩn Senior: Sử dụng Destructuring Assignment
Trong JavaScript/TypeScript hiện đại, hãy để cú pháp destructuring gánh vác việc đó ngay tại chữ ký hàm (function signature).
// ✅ NÊN DÙNG
function createUser({
username,
email,
role = 'user', // Xử lý optional-prop default
isActive = true, // Xử lý optional-prop default
id = generateUUID() // Xử lý optional-prop default
}: CreateUserPayload) {
// Ma thuật của TypeScript nằm ở đây:
// Dù ở CreateUserPayload, `role` và `isActive` là optional (có thể undefined),
// nhưng khi đi vào trong thân hàm, TypeScript tự động hiểu rằng
// `role` chắc chắn là kiểu `string`, và `isActive` là kiểu `boolean`.
console.log(`Đang khởi tạo tài khoản ${username} với quyền ${role}...`);
// Tiến hành lưu vào Database...
}
Lợi ích kép cực lớn:
-
Bảo toàn Type-Safety: Bên ngoài hàm (người gọi API), TypeScript cho phép truyền vào object khuyết role, isActive hợp lệ. Nhưng bên trong hàm, TS tự động thu hẹp kiểu (Type Narrowing) và vứt bỏ type undefined do đã có fallback mặc định. -
Clean Code: Lược bỏ hoàn toàn các câu lệnh if/else kiểm tra undefined hay toán tử ba ngôi dư thừa bên trong logic core.
Tổng kết
Qua bài viết này, hy vọng anh em đã bỏ túi được combo Parameterized Partial (PartialBy) + Destructuring Defaults. Đây không chỉ là một thủ thuật "cứa" vài dòng code, mà nó còn đại diện cho tư duy DRY (Don't Repeat Yourself) và kỹ năng ép kiểu chặt chẽ (Strict Typing) của một kỹ sư TypeScript cứng tay.
Hãy thử áp dụng pattern này vào dự án hiện tại, anh em sẽ thấy file types của mình giảm đi một nửa và code logic dễ thở hơn rất nhiều. Nếu thấy bài viết "gãi đúng chỗ ngứa", đừng tiếc 1 upvote để tiếp thêm năng lượng cho mình ra lò những bài phẫu thuật code tiếp theo nhé! Chúc anh em cuối tuần code mượt, bug ít!
All rights reserved