Menu

Generic trong TypeScript: hàm, kiểu và lớp generic

Generic cho phép một hàm, interface, type hay lớp làm việc với nhiều kiểu mà vẫn giữ chúng gắn kết với nhau: thứ đi vào quyết định thứ đi ra. Tìm hiểu hàm generic, suy luận đối số kiểu, nhiều tham số kiểu, interface và lớp generic, giá trị mặc định, và khi nào không nên dùng generic.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Generic là các tham số kiểu. Một hàm generic khai báo một kiểu giữ chỗ, thường gọi là T, và mỗi lời gọi sẽ điền nó vào, nên kiểu đi vào quyết định kiểu đi ra. Nhờ vậy một hàm dùng được với số, chuỗi và đối tượng do bạn tự định nghĩa, mà mọi lời gọi vẫn có kiểu đầy đủ.

<T> sau tên hàm khai báo tham số kiểu. items: T[] dùng nó cho đầu vào và T | undefined cho đầu ra, gắn hai thứ đó với nhau. Bạn không hề viết number hay string: TypeScript đã suy ra T từ đối số.

Sao không dùng any hay unknown?

Không có generic, bạn sẽ khai báo kiểu tham số là any hoặc unknown. Cả hai đều nhận mọi mảng, và cả hai đều đánh mất mối liên hệ giữa đầu vào và đầu ra:

Kiểu tham sốNhận mọi đầu vàoKiểu kết quảĐược kiểm tra
anycóanykhông: sai sót vẫn biên dịch được
unknowncóunknowncó, nhưng bạn phải thu hẹp kiểu trước khi dùng
T (generic)cógắn với đầu vàocó, với kiểu chính xác

Suy luận đối số kiểu

Thường thì TypeScript suy ra các đối số kiểu từ những giá trị bạn truyền vào. Bạn cũng có thể viết rõ chúng trong dấu ngoặc nhọn khi gọi, điều cần thiết khi không có đối số nào nhắc đến T, hoặc khi việc suy luận chọn ra thứ bạn không muốn.

Có hai kết quả suy luận đáng biết. pair(1, "one") không có đối số kiểu viết rõ là lỗi: TypeScript suy ra T = number từ đối số đầu tiên rồi báo Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). Còn emptyList() không có đối số nào thì nhận T = unknown, cho ra unknown[], hiếm khi là thứ bạn muốn.

Nhiều tham số kiểu

Một hàm có thể khai báo bao nhiêu tham số kiểu tùy ý, ngăn cách bằng dấu phẩy. Mỗi tham số được suy ra độc lập.

mapValues suy ra ba thứ cùng lúc: K là "tea" | "cake", V là number, và R là string từ giá trị trả về của callback. K extends string là một ràng buộc, được trình bày ở trang tiếp theo.

Interface và type alias generic

Kiểu cũng nhận được tham số. Khi đó bạn viết đối số lúc dùng kiểu: Box<number>, ApiResponse<User>. Hầu hết các kiểu tập hợp có sẵn hoạt động theo cách này: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.

Result<T, E = string> cũng cho thấy một tham số kiểu mặc định: Result<number> nghĩa là Result<number, string>. Giống như tham số không bắt buộc của hàm, các tham số có giá trị mặc định phải đứng sau những tham số bắt buộc.

Lớp generic

Một lớp nhận tham số kiểu sau tên của nó, và mỗi instance cố định chúng. Trường, phương thức và tham số của hàm khởi tạo đều dùng được chúng.

Nếu hàm khởi tạo nhận một T, bạn có thể bỏ đối số viết rõ: new Box(5) suy ra Box<number>. Thành viên static thuộc về chính lớp, không thuộc về một instance, nên chúng không dùng được tham số kiểu của lớp (Static members cannot reference class type parameters., TS2302).

Arrow function generic và kiểu hàm generic

Danh sách tham số kiểu đặt trước danh sách tham số. Cùng cú pháp đó mô tả một kiểu hàm generic.

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);

// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];

Suy ra kiểu literal với const type parameter

Mặc định, generic suy ra kiểu đã được mở rộng: pair("a", "b") cho ra [string, string]. Khi giá trị chính xác là quan trọng (tên route, tên sự kiện, danh sách cột), hãy đánh dấu tham số là const (TypeScript 5.0 trở lên) và nó sẽ suy ra như thể đối số được viết kèm as const:

Nơi gọi viết một mảng bình thường, còn hàm giữ lại các kiểu literal.

Khi nào không nên dùng generic

Một tham số kiểu xứng đáng có mặt khi nó kết nối hai thứ: một tham số và kiểu trả về, hai tham số, hoặc một tham số và một callback. Nếu T chỉ xuất hiện một lần, nó chẳng thêm được gì và một kiểu thông thường sẽ rõ ràng hơn.

// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
  console.log(value);
}

// Clearer
function logValueSimple(value: unknown): void {
  console.log(value);
}

// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

Trường hợp cuối rất phổ biến và dễ gây hiểu lầm: parseJson<User>(text) trông có vẻ an toàn kiểu nhưng không kiểm tra gì cả, y như JSON.parse(text) as User. Thay vào đó, hãy trả về unknown rồi xác thực dữ liệu. Những dấu hiệu lạm dụng khác: một tham số kiểu luôn được truyền cùng một đối số, hoặc một tham số có thể thay bằng union của hai kiểu đã biết.

Với những generic cần biết điều gì đó về T (rằng nó có length, hoặc K là một khóa của T), bước tiếp theo là ràng buộc bằng extends.

Câu hỏi thường gặp

Generic trong TypeScript là gì?

Generic là các tham số kiểu: những chỗ giữ chỗ như T được điền bằng một kiểu thật mỗi khi một hàm, interface, type alias hay lớp được dùng. function first<T>(items: T[]): T | undefined dùng được với mọi mảng, và kết quả có kiểu phần tử của mảng bạn truyền vào, nên first([1, 2]) là number | undefined còn first(["a"]) là string | undefined.

Generic và any khác nhau thế nào?

any tắt việc kiểm tra kiểu: giá trị đi vào và giá trị đi ra không có liên hệ gì, và kết quả cũng là any. Generic giữ lại mối liên hệ đó: identity<T>(x: T): T trả về đúng kiểu bạn truyền vào. Dùng unknown nếu bạn nhận mọi thứ nhưng không cần trả kiểu đó ra, và dùng generic nếu có cần.

<T> trong TypeScript nghĩa là gì?

Nó khai báo một tham số kiểu tên là T. Trong function wrap<T>(value: T), T là một biến kiểu mà TypeScript điền từ đối số ở mỗi lời gọi, hoặc bạn truyền rõ ra như wrap<string>("a"). Tên T chỉ là quy ước; những tên dài hơn như TItem hay Key hoạt động y như vậy.

Làm sao để viết arrow function generic trong TypeScript?

Đặt danh sách tham số kiểu trước các tham số: const first = <T>(items: T[]): T | undefined => items[0];. Trong tệp .tsx, <T> sẽ bị đọc thành thẻ JSX, nên hãy viết <T,> với dấu phẩy ở cuối, hoặc <T extends unknown>.

Làm sao để đặt kiểu mặc định cho generic trong TypeScript?

Thêm = Type sau tham số: interface ApiResponse<T = unknown> { data: T }. Khi đó ApiResponse không có đối số nghĩa là ApiResponse<unknown>. Các tham số có giá trị mặc định phải đứng sau những tham số không có, giống như tham số không bắt buộc của hàm.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu