Giới thiệu

Conditional Types là một trong những tính năng mạnh mẽ nhất của TypeScript, cho phép bạn tạo ra các kiểu phụ thuộc vào các điều kiện kiểu khác. Khi làm việc với các API đa dạng, hoặc khi muốn xây dựng các hàm tổng quát, việc dùng Conditional Types giúp giảm thiểu lỗi và tăng tính tái sử dụng của code.

Conditional Types là gì?

Conditional Types có cú pháp A extends B ? X : Y. Nếu kiểu A thỏa mãn B, TypeScript sẽ trả về kiểu X; ngược lại trả về Y. Điều này cho phép bạn mô tả các mối quan hệ phức tạp giữa các kiểu.

Cú pháp cơ bản

Dưới đây là một ví dụ đơn giản minh họa cách tạo kiểu phản hồi API dựa trên việc có lỗi hay không.

type ApiResponse = T extends { error: true }
  ? { success: false; error: string }
  : { success: true; data: T };

function handleResponse<T>(resp: ApiResponse<T>) {
  if ('error' in resp) {
    console.error(resp.error);
  } else {
    console.log(resp.data);
  }
}

// Sử dụng
handleResponse({ success: true, data: { id: 1, name: 'Alice' } });
handleResponse({ error: true, error: 'Not found' });

Áp dụng trong API response

Giả sử bạn có một endpoint trả về dữ liệu người dùng hoặc lỗi. Thay vì viết hai interface riêng, Conditional Types cho phép bạn định nghĩa một kiểu duy nhất, tự động chuyển đổi dựa trên đầu vào.

interface User { id: number; name: string; }
interface NotFoundError { error: true; message: string; }

type GetUserResult = T extends User ? { ok: true; payload: User } : { ok: false; error: string };

function fetchUser<T extends User | NotFoundError>(result: T): GetUserResult<T> {
  if ('error' in result) {
    return { ok: false, error: result.message } as any;
  }
  return { ok: true, payload: result } as any;
}

const r1 = fetchUser({ id: 2, name: 'Bob' }); // { ok: true, payload: { ... } }
const r2 = fetchUser({ error: true, message: 'User not found' }); // { ok: false, error: 'User not found' }

Kết hợp với infer để trích xuất kiểu

Keyword infer chỉ được dùng trong Conditional Types, cho phép bạn “suy ra” một kiểu con từ một kiểu phức tạp. Đây là công cụ hữu ích khi bạn muốn lấy kiểu trả về của một hàm hoặc kiểu phần tử của một mảng.

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

type Foo = ReturnTypeOf<(x: number) => string>; // Foo = string

type ElementType<T> = T extends (infer U)[] ? U : never;

type Num = ElementType<number[]>; // Num = number

Lưu ý khi dùng Conditional Types

  • Tránh lồng quá nhiều Conditional Types lồng nhau, vì chúng có thể làm tăng thời gian biên dịch.
  • Sử dụng never một cách có chủ đích để ngăn các trường hợp không mong muốn.
  • Kiểm tra kết quả bằng type hoặc // @ts-expect-error để chắc chắn kiểu được suy ra đúng.

Kết luận

Conditional Types và infer mở ra khả năng xây dựng các kiểu dữ liệu động, an toàn và dễ bảo trì trong TypeScript. Khi áp dụng đúng cách, chúng giúp giảm thiểu lỗi runtime và tăng tốc độ phát triển. Nếu bạn muốn nắm vững các kỹ thuật này và mở rộng kiến thức TypeScript từ cơ bản đến nâng cao, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.