Quay lại blog
tips7 phút đọc

10 TypeScript Pattern giúp code sạch hơn

Tổng hợp 10 TypeScript pattern thực tiễn — từ discriminated unions, template literal types đến satisfies operator — giúp code type-safe và dễ bảo trì.

V
Bởi Ventra Rocket
·Đăng ngày 15 tháng 2, 2026
#TypeScript#Clean Code#Patterns#Best Practices

TypeScript không chỉ là JavaScript với type annotations. Được dùng đúng cách, TypeScript giúp bắt lỗi tại compile time, cải thiện developer experience và làm cho codebase dễ refactor hơn. Đây là 10 pattern mà đội ngũ Ventra Rocket sử dụng hàng ngày.

1. Discriminated Unions thay vì Boolean Flags

Thay vì dùng nhiều boolean flags dễ gây state không hợp lệ:

// Tệ: Có thể có state không hợp lệ như isLoading=true và error="..."
interface DataState {
  isLoading: boolean;
  data?: User[];
  error?: string;
}

// Tốt: Discriminated union đảm bảo state luôn hợp lệ
type DataState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };

function renderState(state: DataState) {
  switch (state.status) {
    case 'loading': return <Spinner />;
    case 'success': return <UserList users={state.data} />; // data đảm bảo tồn tại
    case 'error':   return <ErrorView message={state.error} />;
    default:        return null;
  }
}

2. Template Literal Types cho Type-Safe Strings

type EventName = `on${Capitalize<string>}`;
type CSSProperty = `${string}-${string}`;
type Locale = 'vi' | 'en';
type LocalizedKey = `${string}.${Locale}`;

// API endpoint typing
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type ApiRoute = `/api/v${number}/${string}`;

function request(method: HttpMethod, url: ApiRoute) { /* ... */ }
request('GET', '/api/v1/users');    // OK
request('GET', '/v1/users');        // Error: not /api/v...

3. Satisfies Operator (TS 4.9+)

// satisfies giữ nguyên literal type nhưng vẫn kiểm tra với interface
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
} satisfies Record<string, string | number>;

// typeof config.apiUrl === 'string' (không phải 'https://api.example.com')
// nhưng đảm bảo tất cả values đều là string | number

const palette = {
  primary: '#06d6a0',
  secondary: '#8b5cf6',
} satisfies Record<string, `#${string}`>;
// Error nếu value không phải hex color

4. Infer trong Conditional Types

// Extract return type của async function
type Awaited<T> = T extends Promise<infer U> ? U : T;

// Extract props type từ React component
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

// Flatten nested array
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Nums = Flatten<number[][]>; // number[]

// Extract route params từ path string
type RouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof RouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type UserRoute = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }

5. Builder Pattern với Method Chaining

class QueryBuilder<T> {
  private filters: Partial<T>[] = [];
  private sortField?: keyof T;
  private limitValue = 50;

  where(filter: Partial<T>): this {
    this.filters.push(filter);
    return this;
  }

  orderBy(field: keyof T): this {
    this.sortField = field;
    return this;
  }

  limit(n: number): this {
    this.limitValue = n;
    return this;
  }

  build(): string {
    // Build SQL/API query
    return JSON.stringify({ filters: this.filters, sort: this.sortField, limit: this.limitValue });
  }
}

const query = new QueryBuilder<User>()
  .where({ role: 'admin' })
  .orderBy('createdAt')
  .limit(20)
  .build();

6. Readonly và Immutability

// Ngăn mutation ngoài ý muốn
type ImmutableUser = Readonly<{
  id: string;
  profile: Readonly<{
    name: string;
    email: string;
  }>;
  permissions: ReadonlyArray<string>;
}>;

// Deep readonly helper
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

7. Function Overloads cho API rõ ràng

function parseInput(input: string): number;
function parseInput(input: number): string;
function parseInput(input: string | number): string | number {
  if (typeof input === 'string') return parseInt(input, 10);
  return input.toString();
}

const num = parseInput('42');  // TypeScript biết return là number
const str = parseInput(42);    // TypeScript biết return là string

8. Mapped Types cho Object Transformation

// Tất cả keys thành optional
type Partial<T> = { [K in keyof T]?: T[K] };

// Tất cả values thành array
type Arrayify<T> = { [K in keyof T]: T[K][] };

// Chỉ lấy method keys
type MethodKeys<T> = {
  [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never
}[keyof T];

// Tạo event map từ state type
type StateEventMap<T> = {
  [K in keyof T as `${string & K}Changed`]: (newValue: T[K]) => void;
};

9. Branded Types cho Domain Primitives

// Ngăn nhầm lẫn giữa các string ID có cùng kiểu dữ liệu
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId   = Brand<string, 'UserId'>;
type OrderId  = Brand<string, 'OrderId'>;
type ProductId = Brand<string, 'ProductId'>;

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const userId = createUserId('user-123');
getUser(userId);    // OK
getOrder(userId);   // Error: UserId không phải OrderId

10. const Assertions và as const

// as const giữ literal types thay vì widening
const ROUTES = {
  home:    '/',
  about:   '/about',
  blog:    '/blog',
  contact: '/contact',
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];
// '/' | '/about' | '/blog' | '/contact'

// Tuple types chính xác
function createPoint(x: number, y: number) {
  return [x, y] as const; // readonly [number, number] thay vì number[]
}

Kết luận

TypeScript mạnh nhất khi bạn để compiler làm việc cho mình. Discriminated unions loại bỏ null checks, branded types ngăn logic bugs, và template literal types đưa runtime errors về compile time. Đầu tư thời gian học các pattern này sẽ giúp codebase của bạn ít bugs hơn và dễ refactor hơn đáng kể.

Bài viết liên quan

10 TypeScript Pattern giúp code sạch hơn | Ventra Rocket