Utility Types trong TypeScript
Bài 13 – Các Utility Types quan trọng nhất trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.
Utility Types trong TypeScript
Utility Types là các kiểu dựng sẵn của TypeScript giúp bạn thao tác nhanh với kiểu dữ liệu: tạo kiểu mới, biến đổi cấu trúc hoặc trích xuất thông tin. Đây là nhóm kiến thức cốt lõi khi làm việc với dự án thực tế, đặc biệt khi thao tác API, form, model dữ liệu, hay build UI phức tạp.
2. Nội dung chính
- Giới thiệu nhóm Utility Types phổ biến
- Partial, Required, Readonly
- Pick, Omit – chọn hoặc bỏ thuộc tính
- Record – tạo map kiểu an toàn
- ReturnType, Parameters – trích xuất kiểu từ hàm
- Ứng dụng thực tế trong API và UI
2. Ví dụ chi tiết
2.1. Base Type - Foundation cho tất cả Utility Types
// Base interface - foundation cho tất cả ví dụ
interface User {
id: number;
name: string;
email: string;
active: boolean;
role: "admin" | "user" | "guest";
createdAt: Date;
updatedAt: Date;
}
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
category: string;
tags: string[];
}
2.2. Partial - Tất cả thuộc tính đều optional
// Partial<T> - tất cả properties đều optional
type UserForm = Partial<User>;
type ProductUpdate = Partial<Product>;
// Ví dụ sử dụng trong forms
const createUserForm: UserForm = {
name: "Alice",
email: "alice@example.com"
// Không cần provide tất cả fields
};
const updateProduct: ProductUpdate = {
price: 99.99,
inStock: false
// Chỉ update những fields cần thiết
};
// Function với Partial
function updateUser(id: number, updates: Partial<User>): User {
// Logic để merge updates với existing user
const existingUser = getUserById(id);
return { ...existingUser, ...updates };
}
// Usage
const updatedUser = updateUser(1, { name: "Alice Smith", active: false });
2.3. Required - Tất cả thuộc tính đều bắt buộc
// Required<T> - tất cả properties đều required
type FullUser = Required<User>;
type CompleteProduct = Required<Product>;
// Sử dụng để validate complete objects
function createUser(userData: Required<User>): User {
// Validate tất cả fields trước khi create
validateUser(userData);
return userData;
}
// Chuyển đổi từ Partial sang Required
const partialUser: Partial<User> = { name: "Bob", email: "bob@example.com" };
const requiredUser: Required<User> = {
id: 1,
name: partialUser.name!,
email: partialUser.email!,
active: true,
role: "user",
createdAt: new Date(),
updatedAt: new Date()
};
2.4. Readonly - Không cho phép thay đổi
// Readonly<T> - tất cả properties đều readonly
type ReadonlyUser = Readonly<User>;
type ImmutableProduct = Readonly<Product>;
const user: ReadonlyUser = {
id: 1,
name: "Bob",
email: "bob@gmail.com",
active: true,
role: "user",
createdAt: new Date(),
updatedAt: new Date()
};
// ❌ Lỗi: Cannot assign to 'name' because it is a read-only thuộc tính
// user.name = "New Name";
// ✅ Phải tạo object mới
const updatedUser: ReadonlyUser = {
...user,
name: "New Name",
updatedAt: new Date()
};
// Deep readonly (custom utility)
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
2.5. Pick<T, K> - Chọn thuộc tính cụ thể
// Pick<T, K> - chọn specific properties
type UserPreview = Pick<User, "id" | "name" | "role">;
type ProductSummary = Pick<Product, "id" | "name" | "price">;
// Sử dụng trong API responses
function getUserPreview(id: number): UserPreview {
const user = getUserById(id);
return {
id: user.id,
name: user.name,
role: user.role
};
}
// Với generic functions
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
// Usage
const user = getUserById(1);
const preview = pick(user, ["id", "name", "role"]);
2.6. Omit<T, K> - Bỏ thuộc tính cụ thể
// Omit<T, K> - omit specific properties
type UserWithoutEmail = Omit<User, "email">;
type ProductWithoutDescription = Omit<Product, "description" | "tags">;
// Sử dụng cho public data (loại bỏ sensitive fields)
type PublicUser = Omit<User, "email" | "createdAt" | "updatedAt">;
function getPublicUser(user: User): PublicUser {
const { email, createdAt, updatedAt, ...publicUser } = user;
return publicUser;
}
// Với forms (loại bỏ auto-generated fields)
type CreateProductData = Omit<Product, "id" | "createdAt" | "updatedAt">;
function createProduct(data: CreateProductData): Product {
return {
...data,
id: generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
}
2.7. Record<K, T> - Tạo map kiểu an toàn
// Record<K, T> - tạo object với known keys
type Role = "admin" | "user" | "guest";
type Permission = "read" | "write" | "delete" | "admin";
type RolePermissions = Record<Role, Permission[]>;
const rolePermissions: RolePermissions = {
admin: ["read", "write", "delete", "admin"],
user: ["read", "write"],
guest: ["read"]
};
// Record với values
type RoleConfig = Record<Role, {
name: string;
description: string;
maxSessions: number;
}>;
const roleConfig: RoleConfig = {
admin: {
name: "Administrator",
description: "Full system access",
maxSessions: 5
},
user: {
name: "Regular User",
description: "Standard user access",
maxSessions: 3
},
guest: {
name: "Guest User",
description: "Limited access",
maxSessions: 1
}
};
// Dynamic record creation
function createRecord<K extends string, T>(
keys: K[],
defaultValue: T
): Record<K, T> {
const record = {} as Record<K, T>;
keys.forEach(key => {
record[key] = defaultValue;
});
return record;
}
const statusCounts = createRecord(["pending", "approved", "rejected"], 0);
// { pending: 0, approved: 0, rejected: 0 }
2.8. ReturnType - Lấy kiểu trả về của hàm
// ReturnType<T> - extract return type từ function
function login(email: string, password: string) {
return {
token: "abc123",
expires: 3600,
user: {
id: 1,
email: email,
name: "John Doe"
}
};
}
type LoginResponse = ReturnType<typeof login>;
// { token: string; expires: number; user: { id: number; email: string; name: string } }
// Với async functions
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
type FetchUserReturn = ReturnType<typeof fetchUser>;
// Promise<any> - cần thêm generic types cho better typing
// Với generic functions
type GenericLoginResponse<T> = ReturnType<typeof loginGeneric<T>>;
2.9. Parameters - Lấy kiểu tham số của hàm
// Parameters<T> - extract parameter types từ function
function sendMail(to: string, subject: string, content: string, options?: {
cc?: string[];
bcc?: string[];
priority?: "low" | "normal" | "high";
}) {
// Implementation
}
type MailParams = Parameters<typeof sendMail>;
// [to: string, subject: string, content: string, options?: { cc?: string[]; bcc?: string[]; priority?: "low" | "normal" | "high" }]
// Sử dụng với destructuring
type FirstParam = Parameters<typeof sendMail>[0]; // string
type OptionsParam = Parameters<typeof sendMail>[3]; // { cc?: string[]; bcc?: string[]; priority?: "low" | "normal" | "high" }
// Với constructor functions
class UserService {
constructor(private apiUrl: string, private timeout: number) {}
}
type UserServiceParams = ConstructorParameters<typeof UserService>;
// [apiUrl: string, timeout: number]
2.10. NonNullable - Loại bỏ null và undefined
// NonNullable<T> - remove null và undefined từ union types
type NullableString = string | null | undefined;
type NonNullableString = NonNullable<NullableString>; // string
// Sử dụng trong functions
function processValue<T>(value: T): NonNullable<T> {
if (value == null) {
throw new Error("Value cannot be null or undefined");
}
return value as NonNullable<T>;
}
// Với arrays
function filterNull<T>(arr: (T | null | undefined)[]): NonNullable<T>[] {
return arr.filter((item): item is NonNullable<T> => item != null);
}
const mixedArray = [1, 2, null, 3, undefined, 4];
const numbersOnly = filterNull(mixedArray); // number[]
3. Kiến thức trọng tâm
Khi nào dùng utility type nào?
| Utility Type | Usage | Ví dụ thực tế |
|---|---|---|
| Partial | Khi cần làm việc với optional properties | Update form data, PATCH API requests |
| Required | Khi cần đảm bảo tất cả fields đều có value | Create/validate complete objects |
| Readonly | Khi object không được phép thay đổi | Configuration objects, constants |
| Pick<T, K> | Khi chỉ cần một số properties | Create summary objects, API responses |
| Omit<T, K> | Khi cần loại bỏ sensitive properties | Public user data, sanitized responses |
| Record<K, T> | Khi cần map keys to values | Object literals, dictionaries |
| ReturnType | Khi cần extract return type | Type-safe function composition |
| Parameters | Khi cần extract parameter types | Higher-order functions, decorators |
| NonNullable | Khi cần loại bỏ null/undefined | Validation, type narrowing |
Best Practices
- Thường xuyên sử dụng utility types thay vì redefine types
- Kết hợp nhiều utility types để tạo complex types
- Đặt tên types rõ ràng khi sử dụng utility types
- Document ý nghĩa của types được tạo từ utility types
Ví dụ tổng hợp
// API Response Handler với utility types
type BaseUser = {
id: number;
email: string;
password: string;
role: string;
createdAt: Date;
updatedAt: Date;
isActive: boolean;
};
// Public user data (remove sensitive fields)
type PublicUser = Omit<BaseUser, "password" | "email">;
// Update payload (all fields optional)
type UpdateUserPayload = Partial<BaseUser>;
// Create payload (required fields only)
type CreateUserPayload = Required<Pick<BaseUser, "email" | "password" | "role">>;
// API Response format
type ApiResponse<T> = {
success: boolean;
data: T;
message?: string;
timestamp: number;
};
// Specific response types
type UserResponse = ApiResponse<PublicUser>;
type UsersResponse = ApiResponse<PublicUser[]>;
// Function types
type CreateUserFunction = (data: CreateUserPayload) => Promise<UserResponse>;
type UpdateUserFunction = (id: number, data: UpdateUserPayload) => Promise<UserResponse>;
4. Bài tập thực hành
Bài 1: Tạo type Product và các biến thể
// Base type
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
category: string;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
// Tạo các utility types
export type ProductUpdate = Partial<Product>;
export type CreateProductData = Omit<Product, "id" | "createdAt" | "updatedAt">;
export type ProductSummary = Pick<Product, "id" | "name" | "price" | "inStock">;
export type ReadonlyProduct = Readonly<Product>;
Bài 2: Tạo ProductUpdate và validation
// Product update với validation
function updateProduct(id: number, updates: ProductUpdate): Product {
const existingProduct = getProductById(id);
// Validate required fields nếu được cung cấp
if (updates.name && updates.name.trim().length === 0) {
throw new Error("Product name cannot be empty");
}
if (updates.price && updates.price < 0) {
throw new Error("Product price cannot be negative");
}
return {
...existingProduct,
...updates,
updatedAt: new Date()
};
}
// Usage
const updated = updateProduct(1, { price: 99.99, inStock: true });
Bài 3: Tạo type PublicUser và Record types
// Public user (loại bỏ sensitive data)
export type PublicUser = Omit<User, "email" | "createdAt" | "updatedAt">;
// Role-based permissions với Record
export type UserRole = "admin" | "user" | "guest";
export type Permission = "read" | "write" | "delete" | "admin";
export type RolePermissions = Record<UserRole, Permission[]>;
export type RoleDisplayNames = Record<UserRole, string>;
export const ROLE_PERMISSIONS: RolePermissions = {
admin: ["read", "write", "delete", "admin"],
user: ["read", "write"],
guest: ["read"]
};
export const ROLE_DISPLAY_NAMES: RoleDisplayNames = {
admin: "Administrator",
user: "Regular User",
guest: "Guest User"
};
// Config với Record
export type Environment = "development" | "staging" | "production";
export type AppConfig = Record<Environment, {
apiUrl: string;
debug: boolean;
timeout: number;
}>;
export const CONFIG: AppConfig = {
development: {
apiUrl: "http://localhost:3000",
debug: true,
timeout: 30000
},
staging: {
apiUrl: "https://staging.api.com",
debug: false,
timeout: 10000
},
production: {
apiUrl: "https://api.example.com",
debug: false,
timeout: 5000
}
};
5. Sai lầm thường gặp
- Lạm dụng Partial - làm mất type safety trong validation
- Quá nhiều nested Omit/Pick - code khó đọc và maintain
- Không document complex utility types - teammates không hiểu intent
- Mix utility types với manual types - inconsistent type definitions
- Quên handle edge cases - missing null/undefined trong unions
⚠️ GHI NHỚ: Khi utility types trở nên quá phức tạp, cân nhắc tạo explicit types
6. Kết luận
Utility Types là công cụ mạnh để tái sử dụng kiểu dữ liệu, giảm lặp lại và tăng độ an toàn cho code. Khi thành thạo các utility types này, bạn sẽ viết TypeScript hiệu quả hơn, type-safe hơn, và dễ maintain hơn.
🔑 GHI NHỚ QUAN TRỌNG:
- Partial/Required cho optional/bắt buộc fields
- Pick/Omit cho field selection/removal
- Readonly cho immutable data
- Record cho key-value mappings
- ReturnType/Parameters cho function type extraction
- Luôn document complex utility type combinations