jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

TypeScript Production · Phần 4 — Generics, Inference & API Design

Thiết kế generic API để compiler suy luận từ input, dùng constraints, const type parameters, overload đúng chỗ và tránh conditional type làm public API khó hiểu.

Generic tốt làm người gọi viết ít type hơn nhưng nhận kết quả chính xác hơn. Generic tệ bắt người gọi truyền type argument, assertion và hiểu implementation của bạn.

Type parameter phải nối ít nhất hai vị trí

function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

T nối input với output. Nếu type parameter chỉ xuất hiện một lần, thường bạn không cần generic.

// thừa generic
function length<T extends { length: number }>(value: T): number {
  return value.length;
}

// rõ hơn
function length(value: { length: number }): number {
  return value.length;
}

Constraint mô tả năng lực tối thiểu

function pluck<T, K extends keyof T>(rows: readonly T[], key: K): Array<T[K]> {
  return rows.map((row) => row[key]);
}

Đừng constraint bằng một domain type lớn nếu algorithm chỉ cần { id: string }. Constraint hẹp tăng khả năng tái dùng và giảm coupling.

Literal inference là một phần UX

function defineRoutes<const T extends Record<string, `/${string}`>>(
  routes: T
): T {
  return routes;
}

const routes = defineRoutes({ home: '/', user: '/users/:id' });
// giữ literal thay vì widen thành string

const type parameter yêu cầu compiler ưu tiên inference literal/readonly. Dùng khi literal là dữ liệu cho API tiếp theo; đừng dùng chỉ để hover trông “xịn”.

satisfies kiểm shape mà không thay type suy luận của expression:

type RouteMap = Record<string, `/${string}`>;

const routes = {
  home: '/',
  users: '/users',
} satisfies RouteMap;

Infer từ dữ liệu, không bắt khai báo lặp

type EventMap = {
  saved: { id: string };
  failed: { error: Error };
};

class Emitter<Events extends Record<string, unknown>> {
  emit<K extends keyof Events>(name: K, payload: Events[K]) {}
}

const bus = new Emitter<EventMap>();
bus.emit('saved', { id: '1' });

Ở đây caller khai báo event map một lần; key và payload luôn correlated.

Overload hay union?

Dùng union khi output không đổi theo input:

function normalize(value: string | URL): string {
  return value instanceof URL ? value.href : value;
}

Dùng overload khi các call shape có output khác nhau và chỉ vài trường hợp:

function get(id: string): Promise<User>;
function get(ids: readonly string[]): Promise<User[]>;
function get(input: string | readonly string[]): Promise<User | User[]> {
  // implementation
  throw new Error('demo');
}

Nếu overload tăng nhanh theo tích Descartes của options, API đang có quá nhiều mode. Tách function hoặc dùng discriminated options.

Conditional return type: mạnh nhưng đắt

type Output<T> = T extends 'json' ? object : string;

declare function parse<T extends 'json' | 'text'>(mode: T): Output<T>;

Public conditional type hợp lý khi correlation thật sự mang giá trị. Nhưng implementation thường cần assertion vì compiler không thể narrow generic T theo runtime branch như union cụ thể. Đừng đẩy complexity vào library chỉ để tiết kiệm một function name.

NoInfer khi một input không được “bỏ phiếu”

function choose<C extends string>(
  choices: readonly C[],
  fallback: NoInfer<C>
): C {
  return choices.includes(fallback) ? fallback : choices[0]!;
}

choose(['red', 'green'] as const, 'blue'); // lỗi

Không có NoInfer, fallback có thể làm C widen để tự hợp thức hóa chính nó.

API review rubric

  • Caller có phải truyền type mà input đã chứa không?
  • Invalid call fail tại argument gần nguyên nhân hay tạo error dài trong generic internals?
  • Hover type có đọc được không?
  • Thêm option có phá inference hiện tại không?
  • API có inference test và negative test không?

Lab

  1. Refactor một helper có explicit type argument thành inference từ parameter.
  2. Dùng satisfies cho config và chứng minh literal vẫn được giữ.
  3. Viết Emitter<EventMap> với on, off, emit không dùng any.
  4. So sánh diagnostics của overload, union và discriminated options trên một API thật.

Done khi: happy path không cần generic syntax ở call site; invalid path báo lỗi ngắn, đúng vị trí.

Đọc tiếp