Các Thực Hành Tốt Nhất Với TypeScript: Viết Code An Toàn Hơn, Dễ Bảo Trì Hơn
Tìm hiểu các pattern TypeScript quan trọng nhất — strict mode, utility types, generics, discriminated unions, và cách tránh các lối thoát phổ biến trong hệ thống kiểu.
TypeScript đã chuyển từ tùy chọn sang thiết yếu trong hầu hết các dự án JavaScript chuyên nghiệp. Nhưng có một khoảng cách lớn giữa "về mặt kỹ thuật là TypeScript" và "TypeScript thực sự phát hiện được lỗi." Sự khác biệt nằm ở một số pattern giúp khai thác sức mạnh thực sự của hệ thống kiểu.
Bật strict mode — luôn luôn
Thay đổi cấu hình TypeScript có giá trị cao nhất:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict kích hoạt một loạt các kiểm tra:
strictNullChecks—nullvàundefinedkhông thể gán cho các kiểu khácnoImplicitAny— biến không thể ngầm có kiểuanystrictFunctionTypes— kiểm tra tham số hàm chặt chẽ hơnstrictPropertyInitialization— thuộc tính class phải được khởi tạo
Nếu không có strictNullChecks, TypeScript bỏ qua lớp lỗi runtime phổ biến nhất. Hãy bật nó ngay từ ngày đầu — việc thêm vào sau cho một codebase lớn rất đau khổ.
Tránh any — dùng unknown thay thế
any là lối thoát khỏi hệ thống kiểu, dập tắt mọi lỗi và phá vỡ mục đích của TypeScript:
// ❌ any vô hiệu hóa mọi kiểm tra kiểu
function processData(data: any) {
data.nonExistentMethod(); // Không có lỗi! Crash khi chạy.
}
// ✅ unknown buộc bạn phải thu hẹp kiểu trước khi sử dụng
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // An toàn — đã thu hẹp thành string
}
if (data instanceof Error) {
console.log(data.message); // An toàn — đã thu hẹp thành Error
}
}
unknown là phiên bản an toàn về kiểu của any. Nó buộc bạn phải kiểm tra xem mình có gì trước khi sử dụng. Hãy dùng nó cho các phản hồi API, phân tích cú pháp JSON, và bất cứ thứ gì đến từ nguồn bên ngoài.
Discriminated unions để xử lý toàn diện
Một trong những pattern mạnh mẽ nhất của TypeScript:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // state.data có sẵn ở đây
case "error":
return <ErrorMessage msg={state.message} />; // state.message có sẵn ở đây
default:
// TypeScript sẽ báo lỗi ở đây nếu bạn thêm state mới mà không xử lý
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
Phép gán never ở cuối là kiểm tra tính đầy đủ — nếu bạn thêm một biến thể mới vào ApiState mà không xử lý trong switch, TypeScript sẽ báo lỗi lúc biên dịch. Đây là một trong những lưới an toàn có giá trị nhất trong toàn bộ ngôn ngữ.
Utility types
TypeScript đi kèm với các bộ biến đổi kiểu tích hợp sẵn:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Làm tất cả thuộc tính thành tùy chọn
type PartialUser = Partial<User>;
// Làm tất cả thuộc tính thành bắt buộc
type RequiredUser = Required<User>;
// Làm tất cả thuộc tính thành chỉ đọc
type ReadonlyUser = Readonly<User>;
// Chọn các thuộc tính cụ thể
type UserPreview = Pick<User, "id" | "name">;
// Loại trừ các thuộc tính cụ thể
type UserWithoutDates = Omit<User, "createdAt">;
// Tạo kiểu từ giá trị của một object
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// Làm thuộc tính có thể null
type NullableUser = { [K in keyof User]: User[K] | null };
// Record type: object với kiểu key và value cụ thể
type RolePermissions = Record<UserRole, string[]>;
// Extract/Exclude từ union types
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
Generics cho code có thể tái sử dụng và an toàn về kiểu
Generics cho phép bạn viết các hàm và class hoạt động với bất kỳ kiểu nào trong khi vẫn đảm bảo an toàn kiểu hoàn toàn:
// Không dùng generics — mất thông tin kiểu
function first(arr: any[]): any {
return arr[0];
}
// Dùng generics — bảo toàn kiểu
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // kiểu: number | undefined
const str = first(["a", "b"]); // kiểu: string | undefined
Ràng buộc generics
// T phải có thuộc tính 'id'
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K phải là key của T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // kiểu: string
const role = getProperty(user, "role"); // kiểu: string
// getProperty(user, "missing"); // ❌ Lỗi TypeScript — "missing" không phải key
Type guards và narrowing
// Type predicate — type guard tùy chỉnh
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// Sử dụng
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript biết data là User ở đây
}
Để xác thực phản hồi API lúc runtime, hãy kết hợp TypeScript interfaces với xác thực schema runtime bằng Zod. Tạo Zod schemas trực tiếp từ JSON của bạn với công cụ JSON to Zod — dán một ví dụ JSON response và nhận schema an toàn về kiểu ngay lập tức.
Template literal types
TypeScript 4.1+ hỗ trợ thao tác chuỗi ở cấp độ kiểu:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
Toán tử satisfies (TypeScript 4.9+)
Xác thực rằng một giá trị khớp với kiểu mà không mở rộng nó:
type ColorMap = Record<string, string>;
// ❌ Không dùng satisfies — kiểu bị mở rộng thành Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Hoạt động nhưng mất kiểu string
// ✅ Dùng satisfies — xác thực VÀ bảo toàn kiểu literal
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript biết đây là string
colors.nonexistent; // ❌ Lỗi — key không tồn tại
Tạo kiểu từ dữ liệu
Thay vì viết tay TypeScript interfaces từ các phản hồi API:
- Dán một JSON response mẫu vào công cụ JSON to TypeScript để nhận interfaces ngay lập tức
- Hoặc tạo Zod schema với JSON to Zod — Zod tự động suy luận TypeScript types qua
z.infer<typeof schema>
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// Kiểu được suy luận tự động — không trùng lặp
type User = z.infer<typeof UserSchema>;
// Xác thực lúc runtime VÀ cung cấp TypeScript types
const user = UserSchema.parse(await response.json());
Các anti-pattern phổ biến cần tránh
| Anti-pattern | Vấn đề | Cách khắc phục |
|---|---|---|
as any |
Vô hiệu hóa kiểm tra kiểu | Dùng unknown + narrowing |
// @ts-ignore |
Dập tắt lỗi mà không sửa | Sửa lỗi kiểu |
as SomeType (ép kiểu không an toàn) |
Bỏ qua suy luận | Dùng type guards |
interface cho unions |
Không hoạt động với discriminated unions | Dùng type |
object quá chung chung |
Không có cấu trúc | Định nghĩa interface phù hợp |
Function làm kiểu |
Không có thông tin tham số/giá trị trả về | Dùng chữ ký cụ thể |
Cấu hình TypeScript đáng biết
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] trả về T | undefined
"exactOptionalPropertyTypes": true, // phân biệt thiếu vs undefined
"noImplicitReturns": true, // mọi nhánh code đều phải trả về giá trị
"noFallthroughCasesInSwitch": true, // không cho phép switch fallthrough ngoài ý muốn
"verbatimModuleSyntax": true // import type tường minh cho các import chỉ dùng kiểu
}
}
Giá trị của TypeScript tỉ lệ thuận với mức độ nghiêm ngặt bạn sử dụng nó. Các pattern ở đây — strict mode, discriminated unions, generics, kiểm tra tính đầy đủ — chính là thứ phân biệt TypeScript tìm ra lỗi với TypeScript chỉ thêm cú pháp.