Back to blog

Deep Dive: Kiểu Nâng Cao trong TypeScript

typescriptadvanced-typesgenericstype-systemprogramming
Deep Dive: Kiểu Nâng Cao trong TypeScript

Chào mừng đến Deep Dive

Bạn đã học nền tảng TypeScript — kiểu cơ bản, interface, generic và utility type. Bây giờ là lúc đi sâu hơn. Bài viết này bao gồm lập trình ở cấp độ kiểu nâng cao giúp phân biệt TypeScript thông thường với kiến trúc thực sự type-safe.

Đây không phải bài tập lý thuyết. Mỗi pattern ở đây giải quyết một vấn đề thực tế: trích xuất kiểu từ hàm, xây dựng event emitter type-safe, tạo branded ID không thể nhầm lẫn, và sinh kiểu từ template chuỗi.

Điều kiện tiên quyết: Giai đoạn 1: Nền tảng TypeScript Thời gian cam kết: 2-3 giờ (thực hành các ví dụ)

Bạn sẽ học được gì

✅ Conditional type với extends và infer
✅ Mapped type và key remapping
✅ Template literal type cho thao tác chuỗi
✅ Recursive type cho cấu trúc lồng nhau
✅ Discriminated union và exhaustive checking
✅ Branded type cho nominal typing
✅ Variadic tuple type
✅ Const assertion và toán tử satisfies
✅ Pattern thực tế: API client type-safe


1. Conditional Type

Conditional type cho phép bạn diễn đạt logic if/else ở cấp kiểu. Cú pháp giống toán tử ba ngôi:

type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true (string literal extends string)

Distributive Conditional Type

Khi conditional type hoạt động trên union, nó phân phối qua từng thành viên:

type ToArray<T> = T extends unknown ? T[] : never;
 
type Result = ToArray<string | number>;
// string[] | number[]  (KHÔNG PHẢI (string | number)[])

Điều này rất mạnh mẽ để lọc union:

type ExtractStrings<T> = T extends string ? T : never;
 
type Mixed = "hello" | 42 | "world" | true;
type OnlyStrings = ExtractStrings<Mixed>;
// "hello" | "world"

Utility type Extract và Exclude tích hợp sẵn của TypeScript hoạt động chính xác theo cách này:

// Định nghĩa tích hợp sẵn:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
 
type Numbers = Exclude<string | number | boolean, string>;
// number | boolean

Ngăn chặn Distribution

Đôi khi bạn không muốn distribution. Bọc cả hai phía trong tuple:

type IsNever<T> = [T] extends [never] ? true : false;
 
type A = IsNever<never>;        // true
type B = IsNever<string>;       // false
 
// Không có tuple wrapper, `never` phân phối thành `never`
type Broken<T> = T extends never ? true : false;
type C = Broken<never>;         // never (không phải true!)

2. Từ khóa infer

infer cho phép bạn trích xuất kiểu từ bên trong conditional type. Hãy nghĩ về nó như pattern matching cho kiểu.

Trích xuất kiểu trả về

type ReturnOf<T> = T extends (...args: unknown[]) => infer R ? R : never;
 
type A = ReturnOf<() => string>;           // string
type B = ReturnOf<(x: number) => boolean>; // boolean
type C = ReturnOf<string>;                 // never

Đây chính xác là cách ReturnType<T> tích hợp sẵn hoạt động.

Trích xuất kiểu tham số

type FirstParam<T> = T extends (first: infer P, ...rest: unknown[]) => unknown
  ? P
  : never;
 
type A = FirstParam<(name: string, age: number) => void>; // string
type B = FirstParam<() => void>;                           // never

Trích xuất từ Promise

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
 
type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<number>>>;  // number (đệ quy!)
type C = Awaited<string>;                    // string (không phải promise)

Trích xuất từ Array

type ElementOf<T> = T extends (infer E)[] ? E : never;
 
type A = ElementOf<string[]>;          // string
type B = ElementOf<[string, number]>;  // string | number

Ví dụ thực tế: Trích xuất Event Payload

type EventMap = {
  "user:login": { userId: string; timestamp: Date };
  "user:logout": { userId: string };
  "order:created": { orderId: string; total: number };
};
 
type EventPayload<K extends keyof EventMap> = EventMap[K];
 
type LoginPayload = EventPayload<"user:login">;
// { userId: string; timestamp: Date }

3. Mapped Type

Mapped type biến đổi mọi thuộc tính của kiểu hiện có. Bạn đã sử dụng các kiểu tích hợp sẵn như Partial<T> và Readonly<T> — đây là cách chúng hoạt động bên dưới.

Mapped Type cơ bản

// Làm tất cả thuộc tính optional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};
 
// Làm tất cả thuộc tính readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
// Làm tất cả thuộc tính required
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

Modifier -? loại bỏ tính optional. Tương tự, -readonly loại bỏ readonly.

Biến đổi giá trị thuộc tính

interface User {
  name: string;
  age: number;
  email: string;
}
 
// Bọc mỗi thuộc tính trong hàm getter
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

Key Remapping với as

Bạn có thể đổi tên key trong quá trình mapping:

// Thêm prefix "on" cho tất cả key
type Listeners<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    value: T[K]
  ) => void;
};
 
type UserListeners = Listeners<User>;
// {
//   onNameChange: (value: string) => void;
//   onAgeChange: (value: number) => void;
//   onEmailChange: (value: string) => void;
// }

Lọc thuộc tính

Trả về never làm key để lọc bỏ thuộc tính:

// Chỉ giữ thuộc tính kiểu string
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
 
type UserStrings = StringProps<User>;
// { name: string; email: string }

Ví dụ thực tế: Form Validation

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}
 
type FormErrors<T> = {
  [K in keyof T]?: string[];
};
 
type FormTouched<T> = {
  [K in keyof T]?: boolean;
};
 
type FormState<T> = {
  values: T;
  errors: FormErrors<T>;
  touched: FormTouched<T>;
  isValid: boolean;
  isSubmitting: boolean;
};
 
const loginState: FormState<LoginForm> = {
  values: { email: "", password: "", rememberMe: false },
  errors: { email: ["Email is required"] },
  touched: { email: true },
  isValid: false,
  isSubmitting: false,
};

4. Template Literal Type

Template literal type cho phép bạn xây dựng kiểu từ pattern chuỗi — cùng cú pháp template literal bạn dùng trong JavaScript, nhưng ở cấp kiểu.

Thao tác chuỗi cơ bản

type Greeting = `Hello, ${string}`;
 
const a: Greeting = "Hello, World";     // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, World";     // Lỗi!
 
// Kết hợp với union để tạo tổ hợp
type Color = "red" | "green" | "blue";
type Opacity = "light" | "dark";
type Theme = `${Opacity}-${Color}`;
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"

Kiểu thao tác chuỗi tích hợp sẵn

TypeScript cung cấp bốn kiểu thao tác chuỗi intrinsic:

type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"

Pattern Matching với Template Literal

Kết hợp với infer để phân tích chuỗi ở cấp kiểu:

// Trích xuất path parameter từ route string
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;
 
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

Ví dụ thực tế: Type-Safe Event Emitter

type EventName = `${string}:${string}`;
 
type SplitEvent<T extends string> =
  T extends `${infer Domain}:${infer Action}`
    ? { domain: Domain; action: Action }
    : never;
 
type Parsed = SplitEvent<"user:login">;
// { domain: "user"; action: "login" }
 
// Type-safe CSS property builder
type CSSUnit = "px" | "rem" | "em" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;
 
function setWidth(value: CSSValue): void {
  console.log(`Setting width: ${value}`);
}
 
setWidth("100px");   // OK
setWidth("2.5rem");  // OK
// setWidth("100");  // Lỗi: không phải CSSValue hợp lệ

5. Recursive Type

Recursive type tham chiếu đến chính nó — thiết yếu để mô hình hóa cấu trúc dữ liệu lồng nhau như cây, JSON và object lồng sâu.

Kiểu JSON

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };
 
// Kiểu này mô tả chính xác mọi giá trị JSON hợp lệ
const data: JSONValue = {
  name: "Alice",
  age: 30,
  hobbies: ["reading", "coding"],
  address: {
    city: "Portland",
    zip: "97201",
  },
};

Deep Readonly

type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonly<E>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;
 
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  features: string[];
}
 
type ReadonlyConfig = DeepReadonly<Config>;
// Tất cả thuộc tính lồng nhau đều readonly — bao gồm phần tử array

Deep Partial

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
 
// Hữu ích cho ghi đè cấu hình
function mergeConfig(
  base: Config,
  overrides: DeepPartial<Config>
): Config {
  return {
    ...base,
    ...overrides,
    database: {
      ...base.database,
      ...overrides.database,
      credentials: {
        ...base.database.credentials,
        ...overrides.database?.credentials,
      },
    },
    features: overrides.features ?? base.features,
  };
}
 
// Chỉ ghi đè những gì bạn cần
mergeConfig(defaultConfig, {
  database: { port: 5433 },
});

Flatten Nested Array

type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
 
type A = Flatten<number[][][]>;  // number
type B = Flatten<string[]>;      // string
type C = Flatten<boolean>;       // boolean

Đường dẫn Dot-Notation

Nhu cầu phổ biến: lấy tất cả đường dẫn dot-notation hợp lệ cho object lồng nhau:

type Paths<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;
 
interface Settings {
  theme: {
    mode: "light" | "dark";
    colors: {
      primary: string;
      secondary: string;
    };
  };
  notifications: {
    email: boolean;
    push: boolean;
  };
}
 
type SettingPaths = Paths<Settings>;
// "theme" | "theme.mode" | "theme.colors" | "theme.colors.primary"
// | "theme.colors.secondary" | "notifications" | "notifications.email"
// | "notifications.push"

6. Discriminated Union

Discriminated union (hay tagged union) là một trong những pattern kiểu thực tế nhất. Thuộc tính literal chung (discriminant) cho phép TypeScript thu hẹp kiểu tự động.

Pattern cơ bản

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

Kiểm tra Exhaustive

Đảm bảo bạn xử lý mọi variant với kỹ thuật never:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape);
      // Nếu bạn thêm variant shape mới mà không xử lý,
      // TypeScript sẽ báo lỗi ở đây tại compile time
  }
}

Kiểu Result (Xử lý lỗi)

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };
 
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}
 
const result = divide(10, 3);
if (result.ok) {
  console.log(result.value); // TypeScript biết đây là number
} else {
  console.error(result.error); // TypeScript biết đây là string
}

Pattern API Response

type ApiResponse<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string; statusCode: number };
 
function renderUser(response: ApiResponse<User>) {
  switch (response.status) {
    case "loading":
      return <Spinner />;
    case "success":
      return <UserCard user={response.data} />;
    case "error":
      return <ErrorMessage message={response.error} />;
  }
}

7. Branded Type

TypeScript sử dụng structural typing — nếu hai kiểu có cùng cấu trúc, chúng tương thích. Điều này có thể gây lỗi khi bạn muốn phân biệt giữa các giá trị khác nhau về mặt khái niệm.

Vấn đề

type UserId = string;
type OrderId = string;
 
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
 
const userId: UserId = "user_123";
const orderId: OrderId = "order_456";
 
// Biên dịch được nhưng SAI — bạn đang truyền order ID cho getUser!
getUser(orderId); // Không có lỗi! Cả hai đều chỉ là `string`

Giải pháp: Branded Type

Thêm thuộc tính phantom chỉ tồn tại ở cấp kiểu:

// Brand utility
type Brand<T, B extends string> = T & { readonly __brand: B };
 
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
 
// Hàm khởi tạo
function createUserId(id: string): UserId {
  // Thêm validation ở đây nếu cần
  return id as UserId;
}
 
function createOrderId(id: string): OrderId {
  return id as OrderId;
}
 
function getUser(id: UserId): void {
  console.log(`Getting user: ${id}`);
}
 
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
 
getUser(userId);   // OK
// getUser(orderId); // Lỗi: Type 'OrderId' is not assignable to type 'UserId'

Branded Numeric Type

type Meters = Brand<number, "Meters">;
type Kilometers = Brand<number, "Kilometers">;
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;
 
function metersToKilometers(m: Meters): Kilometers {
  return (m / 1000) as Kilometers;
}
 
function celsiusToFahrenheit(c: Celsius): Fahrenheit {
  return (c * 9 / 5 + 32) as Fahrenheit;
}
 
const distance = 5000 as Meters;
const km = metersToKilometers(distance); // OK
 
const temp = 100 as Celsius;
// metersToKilometers(temp); // Lỗi! Không thể truyền Celsius làm Meters

Chuỗi đã validate

type Email = Brand<string, "Email">;
type URL = Brand<string, "URL">;
 
function validateEmail(input: string): Email | null {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(input) ? (input as Email) : null;
}
 
function sendEmail(to: Email, subject: string): void {
  // `to` được đảm bảo là email đã validate
  console.log(`Sending "${subject}" to ${to}`);
}
 
const email = validateEmail("[email protected]");
if (email) {
  sendEmail(email, "Welcome!"); // OK — đã validate
}
// sendEmail("not-validated", "Oops"); // Lỗi!

8. Variadic Tuple Type

Variadic tuple type (TypeScript 4.0+) cho phép bạn làm việc với tuple có độ dài không xác định trong khi vẫn giữ thông tin kiểu.

Spread trong Tuple Type

type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
 
type AB = Concat<[string, number], [boolean]>;
// [string, number, boolean]

Typed Function Composition

type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
 
type A = Head<[string, number, boolean]>;  // string
type B = Tail<[string, number, boolean]>;  // [number, boolean]
type C = Last<[string, number, boolean]>;  // boolean

Ví dụ thực tế: Typed Middleware Pipeline

type Middleware<In, Out> = (input: In) => Out;
 
function pipe<A, B>(m1: Middleware<A, B>): Middleware<A, B>;
function pipe<A, B, C>(
  m1: Middleware<A, B>,
  m2: Middleware<B, C>
): Middleware<A, C>;
function pipe<A, B, C, D>(
  m1: Middleware<A, B>,
  m2: Middleware<B, C>,
  m3: Middleware<C, D>
): Middleware<A, D>;
function pipe(...fns: Middleware<unknown, unknown>[]): Middleware<unknown, unknown> {
  return (input) => fns.reduce((acc, fn) => fn(acc), input);
}
 
const transform = pipe(
  (input: string) => input.length,        // string → number
  (length: number) => length > 5,         // number → boolean
  (isLong: boolean) => isLong ? "yes" : "no" // boolean → string
);
 
const result = transform("TypeScript"); // "yes"

9. Const Assertion và satisfies

as const — Const Assertion

as const làm TypeScript suy luận kiểu hẹp nhất có thể:

// Không có as const
const colors = ["red", "green", "blue"];
// kiểu: string[]
 
// Có as const
const colors = ["red", "green", "blue"] as const;
// kiểu: readonly ["red", "green", "blue"]
 
// Không có as const
const config = { mode: "production", port: 3000 };
// kiểu: { mode: string; port: number }
 
// Có as const
const config = { mode: "production", port: 3000 } as const;
// kiểu: { readonly mode: "production"; readonly port: 3000 }

Suy luận kiểu từ hằng số

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
 
const STATUS_MAP = {
  active: "Active",
  inactive: "Inactive",
  pending: "Pending Review",
} as const;
 
type StatusKey = keyof typeof STATUS_MAP;     // "active" | "inactive" | "pending"
type StatusLabel = (typeof STATUS_MAP)[StatusKey]; // "Active" | "Inactive" | "Pending Review"

satisfies — Kiểm tra kiểu không mở rộng

Toán tử satisfies (TypeScript 4.9+) xác nhận giá trị khớp với kiểu mà không mất thông tin kiểu cụ thể:

type ColorMap = Record<string, string | number[]>;
 
// Dùng type annotation — mất chi tiết
const colorsAnnotated: ColorMap = {
  red: "#ff0000",
  green: [0, 255, 0],
};
colorsAnnotated.red.toUpperCase(); // Lỗi! Có thể là string | number[]
 
// Dùng satisfies — giữ chi tiết
const colorsSatisfied = {
  red: "#ff0000",
  green: [0, 255, 0],
} satisfies ColorMap;
 
colorsSatisfied.red.toUpperCase();  // OK! TypeScript biết đây là string
colorsSatisfied.green.map(x => x);  // OK! TypeScript biết đây là number[]

Kết hợp as const và satisfies

type Route = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
};
 
const routes = {
  getUsers: { path: "/users", method: "GET" },
  createUser: { path: "/users", method: "POST" },
  getUser: { path: "/users/:id", method: "GET" },
} as const satisfies Record<string, Route>;
 
// Truy cập type-safe với kiểu literal chính xác
type GetUsersPath = (typeof routes)["getUsers"]["path"]; // "/users"
type GetUsersMethod = (typeof routes)["getUsers"]["method"]; // "GET"

10. Pattern thực tế: API Client Type-Safe

Hãy kết hợp tất cả vào một ví dụ thực tế — API client type-safe suy luận kiểu request và response từ định nghĩa route.

Bước 1: Định nghĩa API Schema

interface ApiSchema {
  "GET /users": {
    response: User[];
    query: { page?: number; limit?: number };
  };
  "GET /users/:id": {
    response: User;
    params: { id: string };
  };
  "POST /users": {
    response: User;
    body: { name: string; email: string };
  };
  "PUT /users/:id": {
    response: User;
    params: { id: string };
    body: Partial<{ name: string; email: string }>;
  };
  "DELETE /users/:id": {
    response: void;
    params: { id: string };
  };
}
 
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

Bước 2: Trích xuất kiểu từ Route

// Trích xuất HTTP method từ route key
type ExtractMethod<T extends string> =
  T extends `${infer M} ${string}` ? M : never;
 
// Trích xuất path từ route key
type ExtractPath<T extends string> =
  T extends `${string} ${infer P}` ? P : never;
 
// Trích xuất path parameter
type ExtractPathParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractPathParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;
 
// Kiểm thử
type Method = ExtractMethod<"GET /users/:id">; // "GET"
type Path = ExtractPath<"GET /users/:id">;       // "/users/:id"
type Params = ExtractPathParams<"/users/:id">;   // "id"

Bước 3: Xây dựng Client

type ApiEndpoint = keyof ApiSchema;
 
type RequestOptions<E extends ApiEndpoint> = {
  params: "params" extends keyof ApiSchema[E]
    ? ApiSchema[E]["params"]
    : never;
  query: "query" extends keyof ApiSchema[E]
    ? ApiSchema[E]["query"]
    : never;
  body: "body" extends keyof ApiSchema[E]
    ? ApiSchema[E]["body"]
    : never;
};
 
// Loại bỏ trường `never` để caller không cần truyền chúng
type CleanOptions<T> = {
  [K in keyof T as T[K] extends never ? never : K]: T[K];
};
 
type FetchOptions<E extends ApiEndpoint> = CleanOptions<RequestOptions<E>>;
 
async function apiClient<E extends ApiEndpoint>(
  endpoint: E,
  ...args: keyof FetchOptions<E> extends never
    ? []
    : [options: FetchOptions<E>]
): Promise<ApiSchema[E]["response"]> {
  const [method, path] = (endpoint as string).split(" ");
  let url = path;
 
  const options = args[0] as Record<string, unknown> | undefined;
 
  // Thay thế path params
  if (options && "params" in options) {
    const params = options.params as Record<string, string>;
    for (const [key, value] of Object.entries(params)) {
      url = url.replace(`:${key}`, value);
    }
  }
 
  // Thêm query params
  if (options && "query" in options) {
    const query = options.query as Record<string, string>;
    const searchParams = new URLSearchParams(query);
    url += `?${searchParams.toString()}`;
  }
 
  const response = await fetch(`/api${url}`, {
    method,
    headers: { "Content-Type": "application/json" },
    body: options && "body" in options
      ? JSON.stringify(options.body)
      : undefined,
  });
 
  return response.json();
}

Bước 4: Sử dụng Client

// GET /users — query params tùy chọn
const users = await apiClient("GET /users", {
  query: { page: 1, limit: 10 },
});
// users: User[]
 
// GET /users/:id — yêu cầu params
const user = await apiClient("GET /users/:id", {
  params: { id: "123" },
});
// user: User
 
// POST /users — yêu cầu body
const newUser = await apiClient("POST /users", {
  body: { name: "Alice", email: "[email protected]" },
});
// newUser: User
 
// DELETE /users/:id — yêu cầu params
await apiClient("DELETE /users/:id", {
  params: { id: "123" },
});
// void
 
// Lỗi kiểu được bắt tại compile time:
// apiClient("GET /users/:id"); // Lỗi: thiếu params
// apiClient("POST /users", { body: { name: 123 } }); // Lỗi: name phải là string
// apiClient("PATCH /users"); // Lỗi: endpoint không tồn tại

Mỗi route đều được định kiểu đầy đủ — params, query, body và response. Thêm route mới vào ApiSchema và client tự động biết cách gọi nó.


Lỗi thường gặp

1. Kiểu quá phức tạp

Không phải mọi vấn đề đều cần kiểu nâng cao. Nếu một kiểu mất hơn 10 giây để hiểu, hãy cân nhắc đơn giản hóa:

// Quá phức tạp — khó đọc và bảo trì
type DeepMerge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof A & keyof B
    ? A[K] extends object
      ? B[K] extends object
        ? DeepMerge<A[K], B[K]>
        : B[K]
      : B[K]
    : K extends keyof B
      ? B[K]
      : K extends keyof A
        ? A[K]
        : never;
};
 
// Thường đơn giản hơn: dùng hàm runtime với kiểu trả về hợp lý
function deepMerge<T extends object>(base: T, overrides: DeepPartial<T>): T {
  // ... implementation
}

2. Tham chiếu kiểu vòng tròn

TypeScript có giới hạn độ sâu đệ quy. Đệ quy sâu có thể gây:

// Có thể gây lỗi "Type instantiation is excessively deep"
type DeepPath<T, Depth extends number[] = []> =
  Depth["length"] extends 10 ? never : // Thêm giới hạn độ sâu
  T extends object
    ? { [K in keyof T & string]:
        | K
        | `${K}.${DeepPath<T[K], [...Depth, 0]>}` }[keyof T & string]
    : never;

3. Lạm dụng as

Type assertion (as) bỏ qua type checker. Mỗi as là một lỗi tiềm ẩn:

// Tránh
const data = fetchData() as User;
 
// Ưu tiên: validate tại runtime
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "name" in data &&
    "email" in data
  );
}
 
const data = fetchData();
if (isUser(data)) {
  // TypeScript biết data là User
  console.log(data.name);
}

Tổng kết và điểm chính

✅ Conditional type: If/else ở cấp kiểu với extends — xử lý union distributive
✅ Từ khóa infer: Trích xuất kiểu từ cấu trúc phức tạp — return type, tham số, promise
✅ Mapped type: Biến đổi tất cả thuộc tính — với key remapping và lọc
✅ Template literal type: Thao tác chuỗi ở cấp kiểu — phân tích route, xây dựng event name
✅ Recursive type: Mô hình hóa dữ liệu lồng nhau — JSON, deep readonly, đường dẫn dot-notation
✅ Discriminated union: Switch/case type-safe với exhaustive checking
✅ Branded type: Nominal typing ngăn nhầm lẫn ID/đơn vị
✅ Variadic tuple: Làm việc với tuple có độ dài không xác định
✅ as const + satisfies: Thu hẹp hằng số trong khi validate với kiểu

Khi nào dùng kiểu nâng cao

PatternSử dụng khi
Conditional typeXây dựng utility kiểu, lọc union
inferTrích xuất kiểu từ hàm, promise, array
Mapped typeBiến đổi API interface, form type
Template literalRoute typing, event system, CSS-in-JS
Branded typeID, đơn vị, chuỗi đã validate
Discriminated unionState machine, API response, action type

Bước tiếp theo

Bây giờ bạn đã nắm vững kiểu nâng cao, hãy áp dụng chúng vào framework thực tế:

Tài liệu tham khảo


Series: Lộ trình TypeScript Full-Stack Trước: Giai đoạn 4: Tích hợp Full-Stack Tiếp: Deep Dive: React với TypeScript

Happy coding! 🚀

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.