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à debug

Reimplement:

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 | boolean

Reimplement:

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-only

Reimplement:

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

#Khi nào dùng cái nào?

Tình huốngUtility Type
Update một phần objectPartial<T>
Chuyển optional → requiredRequired<T>
Lấy vài fields từ typePick<T, K>
Bỏ vài fields khỏi typeOmit<T, K>
Tạo object với key cố địnhRecord<K, T>
Loại bỏ type khỏi unionExclude<T, U>
Giữ type khớp trong unionExtract<T, U>
Loại bỏ null/undefinedNonNullable<T>
Lấy return typeReturnType<T>
Lấy parameter typesParameters<T>
Unwrap PromiseAwaited<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>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 PickExclude 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
  • infer keyword 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 Partial khi 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.