TypeScript cơ bản – Generics và keyof: tái sử dụng mà vẫn giữ kiểu
- 17-10-2026
- Toanngo92
- 0 Comments
Giả sử bạn cần lấy phần tử đầu tiên của mảng tên khóa học, rồi lại cần làm điều tương tự với mảng số. Viết hai hàm giống nhau gây lặp; dùng any làm mất thông tin kiểu. Generics mô tả mối liên hệ giữa đầu vào và đầu ra để một định nghĩa có thể làm việc với nhiều kiểu cụ thể.
Mục lục
1. Từ hàm cụ thể đến hàm generic
function first<T>(items: readonly T[]): T | undefined {
return items[0];
}
const firstTitle = first(["HTML", "TypeScript"]);
const firstId = first([10, 20]);
console.log(firstTitle?.toUpperCase()); // HTML
console.log(firstId); // 10
T là tham số kiểu, không phải một biến nhận giá trị lúc chạy. Trong lần gọi đầu, TypeScript suy luận T là string; lần tiếp theo T là number. Kiểu trả về có undefined vì mảng có thể rỗng. Với noUncheckedIndexedAccess, trình biên dịch cũng giúp nhắc trường hợp này.
Readonly ở tham số cho biết hàm chỉ đọc mảng thông qua tham chiếu này. Không nhất thiết phải truyền kiểu tường minh; khi cần mô tả một mảng rỗng có chủ ý, bạn có thể viết first<string>([]). Kết quả lúc chạy vẫn là undefined, vì khai báo kiểu không tạo phần tử cho mảng.
2. Tái sử dụng cấu trúc dữ liệu
interface Page<T> {
items: T[];
total: number;
}
interface Course {
id: number;
title: string;
}
const page: Page<Course> = {
items: [{ id: 1, title: "TypeScript" }],
total: 1
};
Page mô tả cấu trúc phân trang chung, còn T quyết định kiểu của từng phần tử. Page<Course> và Page<string> chia sẻ cách tổ chức nhưng giữ thông tin khác nhau ở items. Định nghĩa này chỉ kiểm tra cách dùng trong mã; nó không tự xác nhận kết quả trả về từ một API.
3. Ràng buộc bằng extends
function printTitle<T extends { title: string }>(item: T): T {
console.log(item.title);
return item;
}
const course = printTitle({
id: 1,
title: "TypeScript",
free: true
});
console.log(course.free); // true
Nếu T không có ràng buộc, hàm không được giả định item có title. Extends ở đây yêu cầu T có ít nhất thuộc tính title kiểu string; nó không mang nghĩa kế thừa class tại thời điểm chạy. Trả lại T giúp giữ cả id và free trong kiểu kết quả, thay vì làm chúng biến mất khỏi thông tin mà người gọi nhìn thấy.
4. keyof và truy cập thuộc tính có kiểm soát
function getProperty<T, K extends keyof T>(item: T, key: K): T[K] {
return item[key];
}
const title = getProperty(course, "title"); // string
const free = getProperty(course, "free"); // boolean
// getProperty(course, "price"); // Bỏ chú thích sẽ báo lỗi
Keyof T tạo một union các khóa của T. Ràng buộc K extends keyof T giới hạn key vào những khóa hợp lệ. T[K] là kiểu của thuộc tính tương ứng, nên lời gọi lấy title có kết quả string thay vì union của mọi kiểu thuộc tính. Đây là ví dụ nhỏ về kiểu đầu ra phụ thuộc vào đầu vào.
5. Khi nào nên dùng generics?
Dùng khi có một quan hệ kiểu cần được giữ: phần tử và mảng, khóa và object, hoặc dữ liệu và cấu trúc bao quanh. Nếu một hàm chỉ xử lý title và không cần trả lại thông tin của đầu vào, tham số { title: string } có thể đủ. Không cần thêm T vào mọi hàm để làm mã trông nâng cao.
Sau bài này, bạn có thể đọc lại Partial, Pick, Omit và Record trong phần kiểu dữ liệu 3: các ký hiệu trong dấu < > là tham số kiểu. Bước đầu chỉ cần dùng đúng chúng; conditional type và mapped type chuyên sâu có thể học sau. Để chạy các ví dụ cùng nhau, giữ thứ tự định nghĩa và thêm export {} ở cuối src/main.ts.
Đọc thêm: TypeScript: Generics.
Đọc thêm: TypeScript: keyof.
Đọc tiếp trong loạt bài
Bài trước: TypeScript cơ bản – Null, undefined, narrowing và type guards
Bài tiếp theo: TypeScript cơ bản – Module, import, export và tổ chức nhiều file

