TypeScript Generics: Hướng Dẫn Thực Tế Cho Lập Trình Viên

TypeScript Generics: Hướng Dẫn Thực Tế Cho Lập Trình Viên

TypeScript generics là một trong những tính năng mạnh mẽ nhất giúp viết code linh hoạt, tái sử dụng mà vẫn giữ an toàn kiểu. Nếu bạn đã biết TypeScript cơ bản, bài này sẽ giúp bạn hiểu sâu hơn về generics qua ví dụ thực tế.

Generics Là Gì?

Generics cho phép bạn viết hàm, class hoặc interface hoạt động với nhiều kiểu dữ liệu khác nhau thay vì chỉ một kiểu cố định. Thay vì viết function identity(x: number): number, bạn viết một phiên bản tổng quát:

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

Giờ đây identity<number>(42) trả về number, còn identity<string>("hello") trả về string. TypeScript giữ nguyên kiểu dữ liệu qua toàn bộ hàm.

Ví dụ mã TypeScript trên editor IDE

Các Khái Niệm Cơ Bản

Generic Hàm

Generic hàm có thể có nhiều type parameter:

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}
const result = pair<string, number>("age", 30);

Generic Interface

interface Box<T> {
  value: T;
  map<U>(fn: (val: T) => U): Box<U>;
}

Generic Class

class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

Constraints: Giới Hạn Kiểu

Bạn không phải lúc nào cũng muốn chấp nhận mọi kiểu. Constraint giới hạn T phải thỏa mãn điều kiện:

function length<T extends { length: number }>(item: T): number {
  return item.length;
}

Hàm này chấp nhận string, array, hoặc bất kỳ đối tượng nào có thuộc tính length.

Utility Types Với Generics

TypeScript cung cấp nhiều utility type có sẵn:

  • Partial<T> — tất cả thuộc tính trở thành optional
  • Required<T> — tất cả thuộc tính trở thành required
  • Pick<T, Keys> — chọn một số thuộc tính từ T
  • Omit<T, Keys> — bỏ một số thuộc tính từ T
  • Record<Keys, Type> — tạo object type với key-value cụ thể

Ví Dụ Thực Tế: API Response Wrapper

Generics rất hữu ích khi xử lý API response có cấu trúc đồng nhất:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  success: boolean;
}

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

Kiểu User được truyền vào ApiResponse, nên TypeScript biết chính xác data có cấu trúc User.

Điều Kiện Và Inference

Generics hỗ trợ conditional types — kiểu logic if/else ở cấp kiểu:

type IsArray<T> = T extends any[] ? "yes" : "no";
type A = IsArray<string[]>;  // "yes"
type B = IsArray<string>;    // "no"

TypeScript cũng tự động infer generic type trong nhiều trường hợp, không cần chỉ định rõ:

const p = pair("name", 25); // inferred as pair<string, number>

Lỗi Phổ Biến

  • Quá nhiều generics: Nếu hàm có 4+ type parameters, hãy xem lại thiết kế
  • Thiếu constraint: Generics không giới hạn dẫn đến lỗi khi truy cập thuộc tính
  • Không dùng keyof: Khi key phải thuộc kiểu cụ thể, dùng K extends keyof T

Nguồn: TypeScript Handbook – Generics, Total TypeScript, Effective TypeScript (O’Reilly)

Mở Rộng Với Template Literal Types

TypeScript 4.1+ giới thiệu template literal types, kết hợp với generics tạo sức mạnh mới:

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventName<"click">; // "onClick"

function on<T extends string>(event: EventName<T>, handler: () => void) {
  // ...
}

Recursive Generics

TypeScript 4.7+ cho phép generics tham chiếu chính nó, hữu ích cho cây (tree) và danh sách lồng nhau:

interface TreeNode<T> {
  value: T;
  children?: TreeNode<T>[];
}
function flatten<T>(node: TreeNode<T>): T[] {
  return [node.value, ...(node.children?.flatMap(flatten) ?? [])];
}

Thực Hành: Xây Dựng Type-Safe Event Emitter

class EventEmitter<Events extends Record<string, any>> {
  private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
  on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) {
    (this.listeners[event] ??= []).push(handler);
  }
  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    this.listeners[event]?.forEach(h => h(payload));
  }
}
type AppEvents = { login: { userId: string }; message: { text: string } };
const emitter = new EventEmitter<AppEvents>();
emitter.on('login', ({ userId }) => console.log(userId)); // typed!

Với EventEmitter này, TypeScript kiểm tra chính xác event name và payload type, loại bỏ hoàn toàn lỗi typo hay sai kiểu.

Bài Thực Hành: Xây Dựng Hệ Thống Form Type-Safe

Ví dụ thực tế: xây dựng form với generics đảm bảo kiểu dữ liệu:

interface FormField<T> {
  value: T;
  validate: (val: T) => string | null;
  label: string;
}

type Form<F extends Record<string, any>> = {
  [K in keyof F]: FormField<F[K]>;
};

type UserForm = Form<{
  name: string;
  age: number;
  email: string;
}>;

const userForm: UserForm = {
  name: { value: '', validate: (v) => v.length > 0 ? null : 'Bắt buộc', label: 'Tên' },
  age: { value: 0, validate: (v) => v >= 18 ? null : 'Cần 18+', label: 'Tuổi' },
  email: { value: '', validate: (v) => v.includes('@') ? null : 'Invalid', label: 'Email' },
};

function submit<F extends Record<string, any>>(form: Form<F>): F {
  const result: any = {};
  for (const key in form) {
    result[key] = form[key].value;
  }
  return result as F;
}

Generic Và Module Declaration Merging

TypeScript hỗ trợ declaration merging — hai khai báo cùng tên tự động gộp:

interface Array<T> {
  first(): T | undefined;
}
// Giờ mọi mảng đều có phương thức .first()
[1, 2, 3].first(); // 1

Kết hợp generic, ta có thể mở rộng built-in types một cách an toàn.

Code TypeScript trên IDE với syntax highlighting

Tổng Kết

Generics là nền tảng TypeScript nâng cao. Từ hàm đơn giản đến hệ thống form type-safe, generics giúp code linh hoạt hơn mà vẫn giữ được an toàn kiểu. Hãy bắt đầu với generic hàm cơ bản, sau đó dần dần áp dụng vào class, interface, và utility types. Tránh over-engineering — chỉ dùng generic khi thực sự cần tính tái sử dụng.

Nguồn: TypeScript Handbook – Generics, Total TypeScript, Effective TypeScript (O’Reilly), TypeScript Coding Guidelines

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Vite 6 là gì?

Vite 6 là gì? Vite 6 là công cụ build (build tool) và dev server thế hệ mới cho frontend JavaScript. Created bởi Evan You (creator of Vue.js), Vite leverages…

Xem thêm

WebAssembly: Chạy Mã Gốc Trên Trình Duyệt Với Hiệu Suất Gần Bản Địa

WebAssembly (Wasm) là một định dạng mã nhị phân hiệu suất cao, cho phép chạy mã gốc trên trình duyệt web với tốc độ gần như ứng dụng bản địa….

Xem thêm

Flask 3.0: Framework Web Python Nhẹ Nhanh Cho Lập Trình Viên

Flask 3.0 là phiên bản mới nhất của framework web Python nhẹ, nổi tiếng với sự đơn giản và linh hoạt. Flask cung cấp công cụ tối thiểu để xây…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất