[Medium] Kiểu tổng quát (Generics)
1. What are Generics?
Generics là gì?
Generics là một tính năng mạnh mẽ trong TypeScript, cho phép chúng ta tạo các thành phần tái sử dụng có thể xử lý nhiều kiểu dữ liệu thay vì chỉ một kiểu duy nhất.
Khái niệm cốt lõi: Khi định nghĩa hàm, interface hoặc class, không chỉ định trước kiểu cụ thể mà chỉ định khi sử dụng.
Tại sao cần Generics?
Vấn đề khi không có Generics:
// Vấn đề: cần viết một hàm cho mỗi kiểu
function getStringItem(arr: string[]): string {
return arr[0];
}
function getNumberItem(arr: number[]): number {
return arr[0];
}
function getBooleanItem(arr: boolean[]): boolean {
return arr[0];
}
Giải pháp với Generics:
// Một hàm xử lý tất cả các kiểu
function getItem<T>(arr: T[]): T {
return arr[0];
}
getItem<string>(['a', 'b']); // string
getItem<number>([1, 2, 3]); // number
getItem<boolean>([true, false]); // boolean
2. Basic Generic Syntax
Cú pháp Generics cơ bản
Hàm generic
// Cú pháp: <T> biểu thị tham số kiểu
function identity<T>(arg: T): T {
return arg;
}
// Cách sử dụng 1: chỉ định kiểu rõ ràng
let output1 = identity<string>('hello'); // output1: string
// Cách sử dụng 2: để TypeScript suy luận kiểu
let output2 = identity('hello'); // output2: string (tự động suy luận)
Interface generic
interface Box<T> {
value: T;
}
const stringBox: Box<string> = {
value: 'hello',
};
const numberBox: Box<number> = {
value: 42,
};
Class generic
class Container<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T {
return this.items[index];
}
}
const stringContainer = new Container<string>();
stringContainer.add('hello');
stringContainer.add('world');
const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);
3. Generic Constraints
Ràng buộc generic
Ràng buộc cơ bản
Cú pháp: Sử dụng từ khóa extends để giới hạn kiểu generic.
// T phải có thuộc tính length
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength('hello'); // ✅ 5
getLength([1, 2, 3]); // ✅ 3
getLength({ length: 10 }); // ✅ 10
getLength(42); // ❌ Lỗi: number không có thuộc tính length
Ràng buộc với keyof
// K phải là khóa của T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
name: 'John',
age: 30,
email: 'john@example.com',
};
getProperty(user, 'name'); // ✅ 'John'
getProperty(user, 'age'); // ✅ 30
getProperty(user, 'id'); // ❌ Lỗi: 'id' không phải là khóa của user
Nhiều ràng buộc
// T phải thỏa mãn nhiều điều kiện đồng thời
function process<T extends string | number>(value: T): T {
return value;
}
process('hello'); // ✅
process(42); // ✅
process(true); // ❌ Lỗi: boolean nằm ngoài phạm vi ràng buộc
4. Common Interview Questions
Các câu hỏi phỏng vấn thường gặp
Câu hỏi 1: Hiện thực hàm generic
Hãy hiện thực một hàm generic first, trả về phần tử đầu tiên của mảng.
function first<T>(arr: T[]): T | undefined {
// Hiện thực của bạn
}
Nhấn để xem đáp án
function first<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// Ví dụ sử dụng
const firstString = first<string>(['a', 'b', 'c']); // 'a'
const firstNumber = first<number>([1, 2, 3]); // 1
const firstEmpty = first<number>([]); // undefined
Giải thích:
<T>định nghĩa tham số kiểu genericarr: T[]biểu thị mảng kiểu T- Giá trị trả về
T | undefinedbiểu thị có thể là kiểu T hoặc undefined
Câu hỏi 2: Ràng buộc generic
Hãy hiện thực một hàm gộp hai đối tượng, nhưng chỉ gộp các thuộc tính tồn tại trong đối tượng đầu tiên.
function merge<T, U>(obj1: T, obj2: U): T & U {
// Hiện thực của bạn
}
Nhấn để xem đáp án
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 } as T & U;
}
// Ví dụ sử dụng
const obj1 = { name: 'John', age: 30 };
const obj2 = { age: 31, email: 'john@example.com' };
const merged = merge(obj1, obj2);
// { name: 'John', age: 31, email: 'john@example.com' }
Phiên bản nâng cao (chỉ gộp thuộc tính của đối tượng đầu tiên):
function merge<T extends object, U extends Partial<T>>(
obj1: T,
obj2: U
): T {
return { ...obj1, ...obj2 };
}
const obj1 = { name: 'John', age: 30 };
const obj2 = { age: 31 }; // Chỉ có thể chứa thuộc tính của obj1
const merged = merge(obj1, obj2);
// { name: 'John', age: 31 }
Câu hỏi 3: Interface generic
Hãy định nghĩa một interface generic Repository cho các thao tác truy cập dữ liệu.
interface Repository<T> {
// Định nghĩa của bạn
}
Nhấn để xem đáp án
interface Repository<T> {
findById(id: string): T | undefined;
findAll(): T[];
save(entity: T): void;
delete(id: string): void;
}
// Ví dụ hiện thực
class UserRepository implements Repository<User> {
private users: User[] = [];
findById(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
findAll(): User[] {
return this.users;
}
save(entity: User): void {
const index = this.users.findIndex(user => user.id === entity.id);
if (index >= 0) {
this.users[index] = entity;
} else {
this.users.push(entity);
}
}
delete(id: string): void {
this.users = this.users.filter(user => user.id !== id);
}
}
Câu hỏi 4: Ràng buộc generic và keyof
Hãy hiện thực một hàm lấy giá trị thuộc tính của đối tượng theo tên khóa, đảm bảo an toàn kiểu.
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
// Hiện thực của bạn
}
Nhấn để xem đáp án
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Ví dụ sử dụng
const user = {
name: 'John',
age: 30,
email: 'john@example.com',
};
const name = getValue(user, 'name'); // string
const age = getValue(user, 'age'); // number
const email = getValue(user, 'email'); // string
// const id = getValue(user, 'id'); // ❌ Lỗi: 'id' không phải là khóa của user
Giải thích:
K extends keyof Tđảm bảo K phải là một trong các khóa của TT[K]biểu thị kiểu giá trị tương ứng với khóa K trong đối tượng T- Điều này đảm bảo an toàn kiểu, phát hiện lỗi ngay lúc biên dịch
Câu hỏi 5: Kiểu điều kiện và generics
Hãy giải thích kết quả suy luận kiểu của đoạn code sau.
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>;
type B = NonNullable<number | undefined>;
type C = NonNullable<string | number>;
Nhấn để xem đáp án
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<string | number>; // string | number
Giải thích:
NonNullable<T>là một kiểu điều kiện (Conditional Type)- Nếu T có thể gán cho
null | undefinedthì trả vềnever, ngược lại trả vềT - Trong
string | null,stringkhông thỏa điều kiện,nullthỏa điều kiện, nên kết quả làstring - Trong
string | number, cả hai đều không thỏa điều kiện, nên kết quả làstring | number
Ứng dụng thực tế:
function processValue<T>(value: T): NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Value cannot be null or undefined');
}
return value as NonNullable<T>;
}
const result = processValue<string | null>('hello'); // string
5. Advanced Generic Patterns
Các mẫu generic nâng cao
Tham số kiểu mặc định
interface Container<T = string> {
value: T;
}
const container1: Container = { value: 'hello' }; // Sử dụng kiểu mặc định string
const container2: Container<number> = { value: 42 };
Nhiều tham số kiểu
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const numbers = [1, 2, 3];
const strings = map(numbers, (n) => n.toString()); // string[]
Các kiểu tiện ích generic
// Partial: tất cả thuộc tính trở thành tùy chọn
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required: tất cả thuộc tính trở thành bắt buộc
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Pick: chọn các thuộc tính cụ thể
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit: loại trừ các thuộc tính cụ thể
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
6. Best Practices
Thực hành tốt nhất
Cách làm khuyên dùng
// 1. Sử dụng tên generic có ý nghĩa
function process<TData, TResponse>(data: TData): TResponse {
// ...
}
// 2. Sử dụng ràng buộc để giới hạn phạm vi generic
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 3. Cung cấp tham số kiểu mặc định
interface Config<T = string> {
value: T;
}
// 4. Sử dụng các kiểu tiện ích generic
type UserUpdate = Partial<User>;
type UserKeys = keyof User;
Cách làm nên tránh
// 1. Không lạm dụng generics
function process<T>(value: T): T { // ⚠️ Nếu chỉ có một kiểu, không cần generics
return value;
}
// 2. Không sử dụng tên generic một chữ cái (trừ trường hợp đơn giản)
function process<A, B, C>(a: A, b: B, c: C) { // ❌ Ý nghĩa không rõ ràng
// ...
}
// 3. Không bỏ qua ràng buộc
function process<T>(value: T) { // ⚠️ Nếu có giới hạn, nên thêm ràng buộc
return value.length; // Có thể lỗi
}
7. Interview Summary
Tóm tắt phỏng vấn
Tham khảo nhanh
Khái niệm cốt lõi của Generics:
- Không chỉ định kiểu cụ thể khi định nghĩa, chỉ định khi sử dụng
- Cú pháp:
<T>định nghĩa tham số kiểu - Có thể áp dụng cho hàm, interface, class
Ràng buộc generic:
- Sử dụng
extendsđể giới hạn phạm vi generic K extends keyof Tđảm bảo K là khóa của T- Có thể kết hợp nhiều ràng buộc
Các mẫu thường gặp:
- Hàm generic:
function identity<T>(arg: T): T - Interface generic:
interface Box<T> { value: T; } - Class generic:
class Container<T> { ... }
Ví dụ trả lời phỏng vấn
Q: Generics là gì? Tại sao cần Generics?
"Generics là một cơ chế trong TypeScript để tạo các thành phần tái sử dụng, cho phép không chỉ định kiểu cụ thể khi định nghĩa mà chỉ định khi sử dụng. Các ưu điểm chính của Generics là: 1) Tăng khả năng tái sử dụng code - một hàm có thể xử lý nhiều kiểu; 2) Duy trì an toàn kiểu - kiểm tra lỗi kiểu ngay lúc biên dịch; 3) Giảm code trùng lặp - không cần viết một hàm cho mỗi kiểu. Ví dụ
function identity<T>(arg: T): Tcó thể xử lý bất kỳ kiểu nào mà không cần viết hàm riêng cho string, number, v.v."
Q: Ràng buộc generic là gì? Sử dụng như thế nào?
"Ràng buộc generic sử dụng từ khóa
extendsđể giới hạn phạm vi của kiểu generic. Ví dụfunction getLength<T extends { length: number }>(arg: T)đảm bảo T phải có thuộc tính length. Một ràng buộc thường gặp khác làK extends keyof T, đảm bảo K phải là một trong các khóa của T, giúp hiện thực truy cập thuộc tính an toàn kiểu. Ràng buộc giúp chúng ta duy trì an toàn kiểu khi sử dụng generics, đồng thời cung cấp thông tin kiểu cần thiết."