Utility Types toàn tập
Bài 13: Utility Types toàn tập
Bạn đã dùng Partial<User> bao giờ chưa? Hay Pick<User, "name" | "email">? TypeScript có sẵn hơn 15 utility types tích hợp, nhưng đa số dev chỉ dùng 2-3 cái. Bài này sẽ điểm qua tất cả, giải thích từng cái, và — quan trọng hơn — tự reimplement chúng để hiểu cách hoạt động bên trong.
#Partial<T> — Biến mọi property thành optional
interface User {
id: string;
name: string;
email: string;
age: number;
}
// ✅ Tất cả properties đều optional
function updateUser(id: string, updates: Partial<User>) {
// Chỉ cần truyền field muốn update
}
updateUser("1", { name: "An" }); // ✅ OK
updateUser("1", { name: "An", age: 26 }); // ✅ OK
updateUser("1", {}); // ✅ OK — không update gìReimplement:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Test
type PartialUser = MyPartial<User>;
// { id?: string; name?: string; email?: string; age?: number }#Required<T> — Biến mọi property thành bắt buộc
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// ❌ Anti-pattern: dùng Partial khi cần Required
function startServer(config: Partial<Config>) {
// config.host có thể undefined → crash khi dùng
const server = createServer(config.host!); // Non-null assertion — nguy hiểm
}
// ✅ Dùng Required
function startServer(config: Required<Config>) {
// Chắc chắn mọi field đều có giá trị
const server = createServer(config.host); // Safe
}
startServer({ host: "localhost", port: 3000, debug: true }); // ✅
startServer({ host: "localhost" }); // ❌ Error: thiếu port và debugReimplement:
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// -? operator: loại bỏ optional modifier#Pick<T, K> — Chọn một vài properties
// ✅ Chỉ lấy name và email từ User
type UserPreview = Pick<User, "name" | "email">;
// { name: string; email: string }
// Ứng dụng: API response chỉ trả về subset
function getUserPreview(user: User): UserPreview {
return { name: user.name, email: user.email };
}Reimplement:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};#Omit<T, K> — Loại bỏ một vài properties
// ✅ Loại bỏ password khỏi User
type SafeUser = Omit<User & { password: string }, "password">;
// { id: string; name: string; email: string; age: number }
// Ứng dụng: Create DTO — không cần id (server tự generate)
type CreateUserDto = Omit<User, "id">;
// { name: string; email: string; age: number }
function createUser(data: CreateUserDto): User {
return { id: generateId(), ...data };
}Reimplement:
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Omit = Pick + Exclude kết hợp#Record<K, T> — Tạo type từ key và value
// ✅ Tạo object type với key cố định
type UserRole = "admin" | "user" | "guest";
type RolePermissions = Record<UserRole, string[]>;
// {
// admin: string[];
// user: string[];
// guest: string[];
// }
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read"],
guest: ["read"],
};Reimplement:
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
// keyof any = string | number | symbol#Exclude<T, U> — Loại bỏ type từ union
type Status = "active" | "inactive" | "deleted";
// ✅ Loại bỏ "deleted"
type ActiveStatus = Exclude<Status, "deleted">;
// "active" | "inactive"
type AllTypes = string | number | boolean | null | undefined;
type NonNullish = Exclude<AllTypes, null | undefined>;
// string | number | booleanReimplement:
type MyExclude<T, U> = T extends U ? never : T;
// Hoạt động trên distributive conditional type
// "active" extends "deleted" ? never : "active" → "active"
// "deleted" extends "deleted" ? never : "deleted" → never#Extract<T, U> — Giữ lại type khớp
type Status = "active" | "inactive" | "deleted";
// ✅ Chỉ giữ "active" và "deleted"
type SpecialStatus = Extract<Status, "active" | "deleted">;
// "active" | "deleted"Reimplement:
type MyExtract<T, U> = T extends U ? T : never;
// Ngược lại với Exclude#NonNullable<T> — Loại bỏ null và undefined
type MaybeString = string | null | undefined;
// ✅ Chỉ còn string
type DefinitelyString = NonNullable<MaybeString>;
// string
// Ứng dụng: filter null/undefined
function getValues<T>(arr: T[]): NonNullable<T>[] {
return arr.filter((item) => item != null) as NonNullable<T>[];
}
const result = getValues(["a", null, "b", undefined, "c"]);
// type: string[]Reimplement:
type MyNonNullable<T> = T & {};
// Hoặc: Exclude<T, null | undefined>#ReturnType<T> — Lấy return type của function
function createUser() {
return { id: "1", name: "An", email: "an@example.com" };
}
// ✅ Lấy type từ function
type User = ReturnType<typeof createUser>;
// { id: string; name: string; email: string }
// Ứng dụng: không cần khai báo type riêng
type ApiError = ReturnType<typeof createApiError>;Reimplement:
type MyReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
// infer R: "rút trích" return type từ function type#Parameters<T> — Lấy parameter types
function login(username: string, password: string, remember?: boolean) {
// ...
}
// ✅ Lấy tuple type của parameters
type LoginParams = Parameters<typeof login>;
// [username: string, password: string, remember?: boolean]
// Ứng dụng: tạo wrapper function
function withLogging(...args: LoginParams) {
console.log("Calling login with", args);
return login(...args);
}Reimplement:
type MyParameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never;#Awaited<T> — Unwrap Promise type
type Result = Awaited<Promise<Promise<string>>>;
// string — unwrap nested promises
type Data = Awaited<Promise<User[]>>;
// User[]
// Ứng dụng: type cho async function
async function fetchUsers(): Promise<User[]> {
const res = await fetch("/api/users");
return res.json();
}
type Users = Awaited<ReturnType<typeof fetchUsers>>;
// User[]Reimplement:
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
// Recursive unwrap cho nested promises#Readonly<T> — Không cho phép thay đổi
type Config = Readonly<{
host: string;
port: number;
}>;
const config: Config = { host: "localhost", port: 3000 };
config.port = 8080; // ❌ Error: Cannot assign to 'port' because it is read-onlyReimplement:
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};#Khi nào dùng cái nào?
| Tình huống | Utility Type |
|---|---|
| Update một phần object | Partial<T> |
| Chuyển optional → required | Required<T> |
| Lấy vài fields từ type | Pick<T, K> |
| Bỏ vài fields khỏi type | Omit<T, K> |
| Tạo object với key cố định | Record<K, T> |
| Loại bỏ type khỏi union | Exclude<T, U> |
| Giữ type khớp trong union | Extract<T, U> |
| Loại bỏ null/undefined | NonNullable<T> |
| Lấy return type | ReturnType<T> |
| Lấy parameter types | Parameters<T> |
| Unwrap Promise | Awaited<T> |
#Anti-pattern: Partial khi cần Required
// ❌ Anti-pattern
interface CreateUser {
name: string;
email: string;
password: string;
}
function createUser(data: Partial<CreateUser>) {
// data.name có thể undefined → tạo user không có tên!
db.insert({ name: data.name ?? "", email: data.email ?? "" });
}
// ✅ Đúng: dùng Required hoặc không dùng Partial
function createUser(data: CreateUser) {
db.insert(data);
}#Bài tập
#Dễ
Tự implement MyReadonly<T> và MyPick<T, K> mà không dùng utility types tích hợp sẵn.
#Trung bình
Tự implement MyOmit<T, K> bằng cách kết hợp Pick và Exclude tự viết.
#Khó
Viết DeepPartial<T> — biến mọi property (kể cả nested object) thành optional đệ quy.
#Tổng kết
- TypeScript có sẵn utility types: Partial, Required, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited
- Mỗi utility type có thể reimplement bằng mapped types và conditional types
inferkeyword dùng để "rút trích" type từ cấu trúc phức tạp-?modifier loại bỏ optional,+?thêm optional- Không dùng
Partialkhi object cần đầy đủ properties — đó là anti-pattern - Kết hợp utility types để tạo type phức tạp:
Partial<Omit<User, "id">>
Bài tiếp theo: Type-safe với thư viện thật — Zod, React props nâng cao, generic components.