TypeScript best practices in 2026

5 min read

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 unknown thay vì any khi 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

TypeScript 5.x Release Notes

TypeScript Documentation

TypeScript Handbook

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *