•11 min read

TypeScript Generics: Các Mẫu Nâng Cao cho API An Toàn Kiểu

TypeScript Generics: Các Mẫu Nâng Cao cho API An Toàn Kiểu

Generics trong TypeScript là nền tảng để viết mã linh hoạt, có thể tái sử dụng và an toàn về kiểu. Mặc dù các generic cơ bản tương đối dễ hiểu, việc nắm vững các mẫu generic nâng cao sẽ mở khóa sức mạnh thực sự của TypeScript. Vào năm 2026, khi các ứng dụng ngày càng phức tạp hơn, việc tận dụng các kỹ thuật nâng cao này không còn là tùy chọn nữa — mà là điều cần thiết để duy trì các codebase mạnh mẽ.

Trong phần tìm hiểu sâu này, chúng ta sẽ khám phá các mẫu generic nâng cao cho phép bạn tạo ra các API, utility type và generic component có khả năng thích ứng cao, giảm đáng kể lỗi runtime và cải thiện trải nghiệm của nhà phát triển.

Audio Briefing
0:00 / 0:00

Nền tảng: Hiểu về "Biến kiểu"

Trước khi đi sâu vào các mẫu nâng cao, hãy cùng xem lại khái niệm cốt lõi. Một generic cho phép bạn nắm bắt kiểu được cung cấp bởi người dùng và sử dụng nó để xác định kiểu của các đối số, giá trị trả về hoặc thuộc tính.

function identity<T>(arg: T): T {
    return arg;
}

const num = identity<number>(42); // T is number
const str = identity("Hello");    // T is inferred as "Hello" (literal type)
Advertisement

1. Ràng buộc Generic (extends)

Thông thường, bạn không muốn một generic là bất kỳ kiểu nào; bạn cần nó phải có một số thuộc tính nhất định. Đây là lúc các ràng buộc generic xuất hiện bằng cách sử dụng từ khóa extends.

Ràng buộc với một hình dạng đối tượng

interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(arg: T): T {
    console.log(arg.length);
    return arg;
}

logLength("string"); // OK, strings have .length
logLength([1, 2, 3]); // OK, arrays have .length
// logLength(10);    // Error: number doesn't have .length

Ràng buộc theo khóa (keyof)

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // OK, returns string
getProperty(user, "age");  // OK, returns number
// getProperty(user, "email"); // Error: 'email' doesn't exist

Điều này đảm bảo an toàn kiểu tuyệt đối khi truy cập các thuộc tính đối tượng một cách động.

2. Kiểu điều kiện (T extends U ? X : Y)

Kiểu điều kiện cho phép bạn thể hiện các ánh xạ kiểu không đồng nhất dựa trên một điều kiện. Chúng tạo thành cơ sở của nhiều utility type nâng cao.

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

Suy luận trong kiểu điều kiện (infer)

Từ khóa infer trích xuất các kiểu từ bên trong một kiểu khác. Điều này cực kỳ mạnh mẽ để giải nén Promise, trích xuất kiểu trả về của hàm hoặc phân tích mảng.

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type FetchedData = UnwrapPromise<Promise<{ id: number; name: string }>>;
// FetchedData is { id: number; name: string }

// Extract a function's first argument type:
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Fn = (id: number, name: string) => void;
type IdType = FirstArg<Fn>; // number

Kiểu điều kiện phân phối

Khi bạn áp dụng một kiểu điều kiện cho một union, TypeScript sẽ phân phối nó cho từng thành viên:

type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // string[] | number[]
// NOT (string | number)[]

// Prevent distribution by wrapping in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Both = ToArrayNonDist<string | number>; // (string | number)[]

Hiểu về phân phối là rất quan trọng khi viết các utility type hoạt động trên các union.

3. Kiểu ánh xạ

Kiểu ánh xạ tạo các kiểu mới dựa trên các kiểu hiện có bằng cách lặp qua các khóa của chúng.

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};

type Mutable<T> = {
    -readonly [P in keyof T]: T[P]; // remove readonly
};

type RequiredProperties<T> = {
    [P in keyof T]-?: T[P]; // remove optional
};

// Remap keys using 'as'
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
Advertisement

4. Kiểu chuỗi ký tự mẫu

Kiểu chuỗi ký tự mẫu cho phép bạn kết hợp các kiểu chuỗi ký tự bằng cách sử dụng cú pháp mẫu — một trong những bổ sung mạnh mẽ nhất gần đây cho TypeScript.

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
type ChangeEvent = EventName<'change'>; // "onChange"

// Build a full event-handler map from a union:
type AllHandlers = {
    [K in 'click' | 'focus' | 'blur' as EventName<K>]?: (e: Event) => void;
};
// { onClick?: ...; onFocus?: ...; onBlur?: ...; }

Mẫu này cung cấp năng lượng cho các hệ thống sự kiện an toàn kiểu, các trình tạo lớp CSS-in-JS và các trình tạo đường dẫn API mà không có bất kỳ chi phí runtime nào.

5. Kiểu có thương hiệu để đảm bảo an toàn miền

Hệ thống kiểu cấu trúc của TypeScript có nghĩa là hai kiểu có cùng hình dạng có thể hoán đổi cho nhau — ngay cả khi một kiểu là UserId và kiểu kia là OrderId. Kiểu có thương hiệu (còn gọi là kiểu mờ) ngăn chặn việc trộn lẫn ngẫu nhiên:

declare const __brand: unique symbol;
type Brand<T, B> = T & { [__brand]: B };

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

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

const uid  = "u_123" as UserId;
const oid  = "o_456" as OrderId;

getUser(uid);   // ✅ OK
getOrder(oid);  // ✅ OK
// getUser(oid); // ❌ Error: OrderId is not assignable to UserId

Sử dụng các thương hiệu cho ID, số tiền tệ, chuỗi đã được xác thực (ví dụ: EmailAddress), hoặc bất kỳ khái niệm miền nào mà hai kiểu có cấu trúc giống hệt nhau không được nhầm lẫn.

6. Union phân biệt + Kiểm tra đầy đủ

Một union phân biệt ghép nối một ký tự kind (hoặc type) với một payload, cho phép TypeScript tự động thu hẹp trong các câu lệnh switch:

type Circle    = { kind: 'circle';    radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Triangle  = { kind: 'triangle';  base: number; height: number };

type Shape = Circle | Rectangle | Triangle;

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 0.5 * shape.base * shape.height;
        default:
            // exhaustiveness check — compile error if a new Shape is added
            const _exhaustive: never = shape;
            throw new Error(`Unhandled shape: ${JSON.stringify(_exhaustive)}`);
    }
}

Phép gán never trong nhánh mặc định là chìa khóa: nếu bạn thêm type Triangle2 = { kind: 'triangle2'; ... } vào union và quên thêm case, TypeScript sẽ báo lỗi ở dòng _exhaustive trước khi mã chạy.

7. Kiểu Tuple đa dạng

TypeScript 4+ hỗ trợ trải rộng các generic vào các tuple, cho phép suy luận kiểu chính xác cho các hàm chuyển tiếp hoặc biến đổi danh sách đối số:

type Prepend<T, Tuple extends unknown[]> = [T, ...Tuple];

function withLogger<Args extends unknown[], R>(
    fn: (...args: Args) => R,
    label: string
): (...args: Args) => R {
    return (...args: Args) => {
        console.log(`[${label}]`, ...args);
        return fn(...args);
    };
}

const add = (a: number, b: number) => a + b;
const loggedAdd = withLogger(add, 'add');
loggedAdd(2, 3); // TypeScript knows this takes (number, number), not (any, any)

8. Mẫu Builder với Generics

Generics tỏa sáng khi xây dựng các API linh hoạt, nơi ngữ cảnh kiểu phát triển khi các phương thức được xâu chuỗi:

class QueryBuilder<T = Record<string, unknown>> {
    private query: Record<string, unknown> = {};

    select<K extends keyof T>(keys: K[]): QueryBuilder<Pick<T, K>> {
        this.query.select = keys;
        return this as unknown as QueryBuilder<Pick<T, K>>;
    }

    where(condition: Partial<T>): QueryBuilder<T> {
        this.query.where = condition;
        return this;
    }

    execute(): Promise<T[]> {
        // Implementation
        return Promise.resolve([]);
    }
}

interface User { id: number; name: string; age: number; }

// Result is inferred as Promise<Pick<User, 'id' | 'name'>[]>
const result = new QueryBuilder<User>()
    .select(['id', 'name'])
    .where({ age: 30 })
    .execute();

9. Thực tế: Trình bao bọc phản hồi API an toàn kiểu

Đây là một mẫu hoàn chỉnh kết hợp các kiểu điều kiện, kiểu có thương hiệu và union phân biệt cho một client API sản xuất:

// Branded request IDs prevent mixing correlation identifiers
type RequestId = Brand<string, 'RequestId'>;

// Discriminated union response envelope
type ApiResponse<T> =
    | { ok: true;  data: T;       requestId: RequestId }
    | { ok: false; error: string; requestId: RequestId };

// Generic fetch wrapper with full type inference
async function apiFetch<T>(
    url: string,
    schema: (raw: unknown) => T   // runtime validator (e.g. zod.parse)
): Promise<ApiResponse<T>> {
    const requestId = crypto.randomUUID() as RequestId;
    try {
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const raw = await res.json();
        return { ok: true, data: schema(raw), requestId };
    } catch (err) {
        return { ok: false, error: String(err), requestId };
    }
}

// Usage — TypeScript infers the shape of `user` from the schema function
const resp = await apiFetch('/api/users/1', (raw) => {
    if (typeof raw !== 'object' || !raw) throw new Error('invalid');
    return raw as { id: number; name: string };
});

if (resp.ok) {
    console.log(resp.data.name); // fully typed
} else {
    console.error(resp.error);
}

Không có any, không có ép kiểu tại điểm gọi, và bằng chứng tại thời điểm biên dịch rằng mọi đường dẫn thành công đều xử lý data và mọi đường dẫn thất bại đều xử lý error.

10. Higher-Order Components (HOCs) trong React

Khi làm việc với React, generics duy trì an toàn kiểu trong Higher-Order Components:

import React from 'react';

interface WithLoadingProps {
    isLoading: boolean;
}

function withLoading<P extends object>(
    WrappedComponent: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
    return function WithLoadingComponent({ isLoading, ...props }: P & WithLoadingProps) {
        if (isLoading) return <div>Loading...</div>;
        return <WrappedComponent {...(props as P)} />;
    };
}

interface ProfileProps { name: string; }
const Profile = ({ name }: ProfileProps) => <div>{name}</div>;
const ProfileWithLoading = withLoading(Profile);

// <ProfileWithLoading isLoading={true} name="Alice" />
// TypeScript enforces both isLoading and name — nothing is lost.

Tham khảo nhanh

MẫuTrường hợp sử dụng
Ràng buộc extendsHạn chế T với các hình dạng có thuộc tính đã biết
infer trong kiểu điều kiệnTrích xuất các kiểu bên trong (kết quả Promise, kiểu trả về)
Kiểu điều kiện phân phốiÁp dụng các điều kiện trên các thành viên union
Kiểu ánh xạ + asĐổi tên hoặc biến đổi khóa ở cấp độ kiểu
Kiểu chuỗi ký tự mẫuKết hợp chuỗi an toàn kiểu (sự kiện, tuyến đường)
Kiểu có thương hiệuNgăn chặn việc trộn lẫn các kiểu miền có cấu trúc giống hệt nhau
Union phân biệt + neverCác câu lệnh switch được kiểm tra đầy đủ
Tuple đa dạngBảo toàn tính đa dạng trong các hàm bậc cao hơn
Mẫu BuilderCác API linh hoạt nơi kiểu phát triển theo mỗi lần gọi phương thức

Kết luận

Nắm vững generics trong TypeScript sẽ giúp bạn từ việc viết JavaScript được kiểm tra kiểu sang kiến trúc các API mạnh mẽ, tự tài liệu hóa và linh hoạt cao. Các mẫu được đề cập ở đây — kiểu điều kiện, infer, chuỗi ký tự mẫu, kiểu có thương hiệu và union phân biệt — cho phép bạn phát hiện toàn bộ các loại lỗi tại thời điểm biên dịch mà không có bất kỳ chi phí runtime nào.

Khi bạn xây dựng các thư viện và ứng dụng phức tạp hơn, các mẫu generic nâng cao này sẽ trở thành công cụ không thể thiếu trong kho vũ khí TypeScript của bạn.

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Roblox có dùng JavaScript không? Giải thích JavaScript vs Luau (2026)
roblox

Roblox có dùng JavaScript không? Giải thích JavaScript vs Luau (2026)

Roblox sử dụng Luau chứ không phải JavaScript — nhưng nếu bạn biết JavaScript, bạn có thể học Luau rất nhanh. Hướng dẫn này bao gồm các điểm khác biệt chính: kiểu dữ liệu, vòng lặp, phạm vi và các API dành riêng cho Roblox dành cho các nhà phát triển web chuyển sang viết script cho Roblox.

Read more