
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.
Trước khi phân tích các pattern 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)
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.
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
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.
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
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
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.
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; }
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.
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.
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.
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)
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();
Đâ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.
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.
| Mẫu | Trường hợp sử dụng |
|---|---|
Ràng buộc extends | Hạ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ện | Trí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ẫu | Kết hợp chuỗi an toàn kiểu (sự kiện, tuyến đường) |
| Kiểu có thương hiệu | Ngă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 + never | Các câu lệnh switch được kiểm tra đầy đủ |
| Tuple đa dạng | Bảo toàn tính đa dạng trong các hàm bậc cao hơn |
| Mẫu Builder | Các API linh hoạt nơi kiểu phát triển theo mỗi lần gọi phương thức |
Bạn đang xây dựng một thư viện tiện ích được tối ưu hóa cao cho một ứng dụng quy mô lớn. Một trong các hàm của bạn cần chấp nhận một item đơn lẻ hoặc một mảng các item, và luôn trả về một mảng. Bạn muốn đảm bảo suy luận kiểu chính xác nhất có thể cho kiểu trả về, đặc biệt khi xử lý các literal types hoặc các kiểu đối tượng phức tạp. Kiểu tiện ích generic nào sau đây đạt được sự cân bằng tốt nhất giữa tính linh hoạt và độ chính xác của kiểu, có tính đến các tác động tiềm ẩn về hiệu suất đối với các union rất lớ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.
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

Nắm vững các mẫu TypeScript doanh nghiệp nâng cao: branded types, conditional response types, template literal routing và toán tử satisfies.
Đọc tiếp
Hướng dẫn toàn diện về TypeScript 5.8+ Stage 3 Decorator: metaprogramming, DI và xác thực runtime với kiến trúc và ví dụ mã nguồn cấp độ sản xuất.
Đọc tiếp
Mở rộng quy mô API dưới tải lớn đòi hỏi nhiều hơn là các thao tác GET/SET cơ bản. Khám phá các mô hình caching Redis nâng cao để xử lý hàng triệu yêu cầu một cách mượt mà.
Đọc tiếp