Nếu bạn đang viết TypeScript trong môi trường production vào năm 2026, có lẽ bạn đã quá quen với những khái niệm cơ bản như:
- Union Types
- Generics
- Sử dụng
unknownthay vìanykhi không tin tưởng dữ liệu đầu vào
Những điều đó giờ gần như không còn gây tranh cãi nữa.
Điểm khác biệt giữa các đội ngũ hiện nay nằm ở cách họ sử dụng hệ thống type khi dự án bắt đầu phức tạp:
- Refactor liên tục
- API thay đổi
- Dependency được nâng cấp
- Code phải tồn tại qua nhiều thế hệ developer
Bài viết này không nói về các “chiêu trò” TypeScript. Nó tập trung vào một số tính năng nâng cao thực sự mang lại giá trị trong các codebase thực tế. Không phải advanced type nào cũng nên dùng trong production, nhưng những thứ dưới đây rất đáng để áp dụng nếu vẫn giữ được tính dễ đọc.
1. Dùng satisfies cho Config Để Refactor An Toàn Hơn
Thông thường, chúng ta hay khai báo kiểu trực tiếp:
const routes: Record<string, RouteConfig> = {
...
};
Điều này hoạt động được nhưng có một vấn đề:
TypeScript sẽ “widen” kiểu dữ liệu.
Ví dụ:
{
method: "GET"
}
sẽ bị biến thành:
{
method: string
}
Bạn mất đi khả năng theo dõi chính xác literal value.
Thay vào đó:
type RouteConfig = {
method: "GET" | "POST";
path: `/${string}`;
auth: "public" | "user" | "admin";
};
const routes = {
listUsers: {
method: "GET",
path: "/users",
auth: "admin",
},
me: {
method: "GET",
path: "/me",
auth: "user",
},
} satisfies Record<string, RouteConfig>;
Lúc này TypeScript:
- Kiểm tra đúng cấu trúc
- Giữ nguyên literal type
Ví dụ:
type RouteName = keyof typeof routes;
// "listUsers" | "me"
type MePath = typeof routes.me.path;
// "/me"
Rất hữu ích cho:
- Route maps
- Feature flags
- Event catalogs
- Permission tables
⚠️ Lưu ý: satisfies chỉ hoạt động ở compile-time. Nếu dữ liệu đến từ API hoặc file JSON, bạn vẫn cần runtime validation.
2. Template Literal Types Cho Các Chuỗi Có Cấu Trúc
Trong hệ thống thực tế luôn tồn tại những chuỗi theo quy ước:
user:123
org:acme:members
feature:darkmode:enabled
Nếu không có ràng buộc, typo sẽ xuất hiện rất dễ dàng.
Ví dụ:
type CacheKey =
| `user:${string}`
| `org:${string}:members`
| `feature:${string}:enabled`;
Khi đó:
getCache("user:123"); // OK
getCache("users:123");
// Error
Bạn có thể còn tách dữ liệu ra bằng infer:
type ExtractUserId<K> =
K extends `user:${infer Id}`
? Id
: never;
type UserId =
ExtractUserId<"user:123">;
// "123"
Ứng dụng rất tốt cho:
- Cache keys
- Event names
- Analytics events
- Message queues
- Routing
⚠️ Không nên lạm dụng để mô hình hóa mọi chuỗi trong hệ thống vì sẽ làm type trở nên khó đọc và chậm compile.
3. Assertion Functions Để Kết Nối Runtime Validation Với Type Narrowing
Một vấn đề phổ biến:
Bạn validate dữ liệu runtime rồi nhưng TypeScript không biết điều đó.
Ví dụ:
function assertNonEmptyString(
value: unknown,
field: string
): asserts value is string {
if (
typeof value !== "string" ||
value.trim() === ""
) {
throw new Error(
`${field} must be a non-empty string`
);
}
}
Sau đó:
const name =
(body as { name?: unknown }).name;
assertNonEmptyString(name, "name");
// Từ đây TypeScript hiểu:
// name là string
Điều này giúp:
- Validate ở boundary
- Không cần check lặp lại
- Code bên trong sạch hơn
Rất hữu ích khi xử lý:
- Request body
- Query params
- Environment variables
- Third-party SDK
⚠️ Với schema lớn vẫn nên dùng:
- Zod
- Valibot
- Yup
thay vì tự viết quá nhiều assertion functions.
4. Exhaustiveness Checking Với never
Một bug cực kỳ phổ biến khi refactor:
Bạn thêm một case mới vào union nhưng quên xử lý ở một nơi nào đó.
Ví dụ:
type Action =
| { type: "added"; id: string }
| { type: "removed"; id: string };
Tạo helper:
function assertNever(
x: never
): never {
throw new Error(
`Unhandled action`
);
}
Sử dụng:
function reducer(
action: Action
) {
switch (action.type) {
case "added":
return;
case "removed":
return;
default:
return assertNever(action);
}
}
Nếu sau này thêm:
{
type: "updated";
}

TypeScript sẽ báo lỗi ngay lập tức.
Đây là một trong những kỹ thuật đáng dùng nhất khi làm reducer hoặc state machine.
⚠️ Gần như không có nhược điểm nào ngoài việc nhớ sử dụng nó.
5. Branded Types Cho Các ID Không Được Phép Trộn Lẫn
Trong rất nhiều hệ thống:
UserId = string
OrderId = string
ProductId = string
Điều này rất dễ dẫn đến bug.
Ví dụ:
fetchOrder(
orderId,
userId
);
TypeScript vẫn cho phép vì tất cả đều là string.
Giải pháp:
declare const userIdBrand:
unique symbol;
declare const orderIdBrand:
unique symbol;
type UserId =
string & {
readonly [userIdBrand]:
"UserId";
};
type OrderId =
string & {
readonly [orderIdBrand]:
"OrderId";
};
Lúc này:
fetchOrder(
"u_123" as UserId,
"o_999" as OrderId
);
OK.
fetchOrder(
"o_999" as OrderId,
"u_123" as UserId
);
TypeScript báo lỗi.
Rất phù hợp cho:
- Billing
- Identity
- Permission system
- Financial system
⚠️ Branded Type không phải validation. Nó chỉ ngăn việc dùng nhầm type trong code.
6. Conditional Types Và infer
Một utility kinh điển:
type UnwrapPromise<T> =
T extends Promise<infer U>
? U
: T;
Ví dụ:
type A =
UnwrapPromise<
Promise<number>
>;
// number
type B =
UnwrapPromise<string>;
// string
Ở đây:
infer U
có nghĩa:
“Hãy lấy type bên trong Promise và gán tên cho nó là U”
Kỹ thuật này rất hữu ích khi xây dựng:
- API wrappers
- SDK
- Generic libraries
- Utility types
⚠️ Nếu đồng đội không thể giải thích type đó trong một câu ngắn gọn thì có lẽ nó không nên xuất hiện trong code shared.
Những Điều Nên Tránh Dù Đã Là Năm 2026
Một số dấu hiệu cho thấy bạn đang đi quá xa với TypeScript:
1. Type khó hiểu hơn chính đoạn code
Nếu mất 5 phút để hiểu type nhưng chỉ mất 10 giây để hiểu function thì có lẽ type đó quá phức tạp.
2. Dùng TypeScript thay cho Runtime Validation
TypeScript không kiểm tra dữ liệu runtime.
Nếu API trả sai dữ liệu:
const user: User = response;
TypeScript vẫn không cứu được bạn.
3. Utility Types Không Có Chủ Sở Hữu
Những helper type được copy khắp nơi thường trở thành “technical debt” sau vài năm.
Không ai dám sửa vì không biết ảnh hưởng đến đâu.
Kết Luận
TypeScript mạnh nhất khi nó hoạt động ở hậu trường.
Mục tiêu không phải là tạo ra những type “thông minh nhất”, mà là:
- Refactor an toàn hơn
- Bắt lỗi sớm hơn
- Giảm bug khi thay đổi hệ thống
- Giúp developer mới hiểu code nhanh hơn
Một nguyên tắc rất hay từ bài viết:
“Nếu đồng đội không thể giải thích type đó trong một câu ngắn gọn, thì có lẽ nó không nên xuất hiện trong code dùng chung.”
TypeScript Official
