# Kiểu Generics là gì hãy giải thích rõ và vĩ dụ giúp tôi dễ hiểu?

### **Generics là gì?**

**Generics** là một tính năng trong TypeScript (và nhiều ngôn ngữ lập trình khác như Java, C#) cho phép bạn tạo ra các **hàm, lớp, hoặc interface** có thể hoạt động với nhiều kiểu dữ liệu khác nhau mà vẫn giữ được tính an toàn về kiểu (**type safety**).

Thay vì định nghĩa một hàm hoặc lớp chỉ làm việc với một kiểu cụ thể, bạn có thể sử dụng Generics để làm cho nó linh hoạt hơn.

### **Ví dụ đơn giản về Generics**

#### **Không dùng Generics**

Ví dụ, bạn muốn viết một hàm trả về đúng giá trị mà nó nhận vào:

```typescript
function identity(value: number): number {
    return value;
}

```

Hàm trên chỉ hoạt động với kiểu `number`. Nếu bạn muốn hỗ trợ cả `string`, bạn phải viết thêm hàm khác:

```typescript
function identityString(value: string): string {
    return value;
}
```

Điều này không tối ưu, vì bạn phải viết nhiều hàm có cùng logic nhưng khác kiểu.

#### **Dùng Generics**

Bạn có thể dùng Generics để viết một hàm tổng quát:

```typescript
function identity<T>(value: T): T {
    return value;
}
```

Ở đây:

* `<T>` là một tham số kiểu (**type parameter**), cho phép `T` đại diện cho bất kỳ kiểu dữ liệu nào.
* `value: T` nghĩa là giá trị đầu vào có kiểu `T`.
* `T` cũng được dùng làm kiểu trả về, đảm bảo hàm trả về đúng kiểu của đầu vào.

Bây giờ bạn có thể gọi hàm này với bất kỳ kiểu dữ liệu nào:

```typescript
console.log(identity<number>(42));   // Output: 42
console.log(identity<string>("Hello")); // Output: Hello
console.log(identity<boolean>(true));   // Output: true
```

TypeScript sẽ tự suy luận kiểu nếu không cung cấp `<T>`:

```typescript
console.log(identity(100));  // TypeScript hiểu T là number
console.log(identity("Test"));  // TypeScript hiểu T là string
```

### **Generics với Array và Interface**

#### **1. Generics với Mảng**

Nếu bạn muốn viết một hàm trả về một mảng chứa một phần tử đầu vào:

```typescript
function toArray<T>(element: T): T[] {
    return [element];
}

console.log(toArray<number>(5));    // Output: [5]
console.log(toArray<string>("Hi")); // Output: ["Hi"]
```

#### **2. Generics với Interface**

Giả sử bạn có một interface đại diện cho một hộp chứa một giá trị:

```typescript
interface Box<T> {
    value: T;
}
const numberBox: Box<number> = { value: 10 };
const stringBox: Box<string> = { value: "Hello" };
console.log(numberBox.value); // Output: 10
console.log(stringBox.value); // Output: Hello

```

Interface `Box<T>` giúp tạo ra một cấu trúc linh hoạt có thể chứa bất kỳ kiểu dữ liệu nào.

### **Generics với Class**

Bạn có thể dùng Generics với class để tạo một cấu trúc dữ liệu linh hoạt hơn, chẳng hạn như một `Storage` lưu trữ các giá trị:

```typescript
class Storage<T> {
    private items: T[] = [];

    add(item: T): void {
        this.items.push(item);
    }

    getAll(): T[] {
        return this.items;
    }
}
const numberStorage = new Storage<number>();
numberStorage.add(10);
numberStorage.add(20);
console.log(numberStorage.getAll()); // Output: [10, 20]

const stringStorage = new Storage<string>();
stringStorage.add("A");
stringStorage.add("B");
console.log(stringStorage.getAll()); // Output: ["A", "B"]

```

### **Generics với Constraint (Ràng buộc kiểu)**

Khi sử dụng Generics, bạn có thể muốn giới hạn kiểu dữ liệu. Ví dụ, chỉ cho phép `T` là một object có thuộc tính `length`:

```typescript
function logLength<T extends { length: number }>(value: T): void {
    console.log(value.length);
}

logLength("Hello");      // Output: 5
logLength([1, 2, 3]);    // Output: 3
logLength({ length: 10 }); // Output: 10
// logLength(42); // Lỗi vì số không có thuộc tính length

```

Ở đây, `T extends { length: number }` đảm bảo `T` có thuộc tính `length`.

### **Tóm tắt**

* **Generics** giúp viết code linh hoạt, tái sử dụng và an toàn hơn về kiểu.
* Dùng **\<T>** để tạo Generics trong **hàm, class, interface**.
* Generics có thể hoạt động với **mảng, object, interface, class**.
* Có thể dùng **constraint (ràng buộc kiểu)** để giới hạn kiểu dữ liệu.

Bạn đã hiểu rõ hơn chưa? Có muốn mở rộng thêm phần nào không? 😊

### Mở rộng cho tôi phần extends và dùng 2 Generics cùng một lúc

### **Mở rộng về `extends` trong Generics và sử dụng nhiều Generics cùng lúc**

#### **1. `extends` trong Generics (Ràng buộc kiểu)**

Thông thường, khi sử dụng Generics, TypeScript cho phép bất kỳ kiểu nào. Nhưng đôi khi, bạn muốn giới hạn Generics để chỉ chấp nhận những kiểu có thuộc tính hoặc phương thức nhất định. Đó là khi **`extends`** được sử dụng.

#### **Ví dụ: Ràng buộc Generics với `extends`**

Bạn muốn đảm bảo rằng tham số `T` phải có thuộc tính `length`:

```typescript
function logLength<T extends { length: number }>(value: T): void {
    console.log(value.length);
}
logLength("Hello");      // Output: 5
logLength([1, 2, 3]);    // Output: 3
logLength({ length: 10 }); // Output: 10
// logLength(42); // ❌ Lỗi vì số không có thuộc tính length

```

Ở đây:

* `T extends { length: number }` giới hạn `T` chỉ có thể là kiểu có thuộc tính `length`, chẳng hạn như **string, array, object có length**.

#### **2. `extends` với Interface**

Bạn có thể ràng buộc Generics bằng một interface.

**Ví dụ: Đảm bảo T phải có thuộc tính `name`**

```typescript
interface HasName {
    name: string;
}
function greet<T extends HasName>(obj: T): void {
    console.log("Hello, " + obj.name);
}
greet({ name: "Alice" }); // ✅ Output: Hello, Alice
greet({ name: "Bob", age: 25 }); // ✅ Vẫn hợp lệ vì có thuộc tính name
// greet({ age: 30 }); // ❌ Lỗi vì không có name

```

Ở đây:

* `T` phải là một object có thuộc tính `name`.

#### **3. Dùng 2 Generics cùng lúc**

Bạn có thể sử dụng nhiều Generics bằng cách định nghĩa nhiều tham số kiểu.

**Ví dụ 1: Hàm nhận 2 kiểu dữ liệu khác nhau**

```typescript
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}
console.log(pair<number, string>(1, "one")); // Output: [1, "one"]
console.log(pair<boolean, string>(true, "yes")); // Output: [true, "yes"]

```

Ở đây:

* `T` là kiểu của `first`
* `U` là kiểu của `second`
* Hàm trả về một tuple `[T, U]`

**Ví dụ 2: Dùng 2 Generics với Interface**

Bạn có thể sử dụng hai Generics để tạo một interface có thể chứa hai kiểu dữ liệu khác nhau.

```typescript
interface KeyValue<K, V> {
    key: K;
    value: V;
}
const item1: KeyValue<number, string> = { key: 1, value: "One" };
const item2: KeyValue<string, boolean> = { key: "isDone", value: true };
console.log(item1); // Output: { key: 1, value: "One" }
console.log(item2); // Output: { key: "isDone", value: true }

```

#### **4. Kết hợp `extends` với nhiều Generics**

Bạn có thể ràng buộc **một hoặc cả hai** tham số kiểu.

**Ví dụ: Một kiểu có ràng buộc, một kiểu không**

```typescript
function merge<T extends object, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

const merged = merge({ name: "Alice" }, { age: 30 });
console.log(merged); // Output: { name: "Alice", age: 30 }

// merge(10, { age: 30 }); // ❌ Lỗi vì 10 không phải object
```

Ở đây:

* `T extends object` đảm bảo `obj1` phải là object.
* `U` có thể là bất kỳ kiểu nào.

### Ví dụ: Cả hai kiểu đều có ràng buộc

```typescript
interface Person {
    name: string;
}
interface Job {
    title: string;
}
function createEmployee<T extends Person, U extends Job>(person: T, job: U) {
    return { ...person, ...job };
}
const employee = createEmployee({ name: "Alice" }, { title: "Developer" });
console.log(employee); // Output: { name: "Alice", title: "Developer" }

// createEmployee({ age: 30 }, { title: "Manager" }); // ❌ Lỗi vì không có `name`

```

Ở đây:

* `T extends Person` đảm bảo `T` có thuộc tính `name`.
* `U extends Job` đảm bảo `U` có thuộc tính `title`.

### **Tóm tắt**

* **`extends` trong Generics** giúp giới hạn kiểu dữ liệu, đảm bảo rằng nó có các thuộc tính cụ thể.
* **Dùng nhiều Generics (`<T, U>`)** giúp viết code linh hoạt hơn, hỗ trợ nhiều kiểu dữ liệu cùng lúc.
* **Có thể kết hợp `extends` với nhiều Generics** để tạo ra những API mạnh mẽ.

Bạn đã hiểu rõ hơn chưa? Nếu cần ví dụ thực tế hơn, mình có thể giúp nhé! 🚀

### 😁 extends keyof là gì

### **`extends keyof` trong Generics – Giải thích và Ví dụ**

#### **1. `keyof` trong TypeScript là gì?**

`keyof` trong TypeScript được dùng để lấy danh sách **các key của một object** dưới dạng **union type**.

**Ví dụ:**

```ts
tsCopyEdittype Person = { name: string; age: number };
type PersonKeys = keyof Person; // "name" | "age"

let key: PersonKeys;
key = "name"; // ✅ Hợp lệ
key = "age";  // ✅ Hợp lệ
// key = "email"; // ❌ Lỗi vì "email" không có trong Person
```

* `keyof Person` lấy ra `"name" | "age"`, tức là tất cả các key của `Person`.

***

#### **2. Kết hợp `extends` với `keyof` trong Generics**

Khi dùng Generics, bạn có thể **giới hạn một kiểu dữ liệu chỉ được phép là key của một object** bằng cách dùng `T extends keyof U`.

**Ví dụ 1: Lấy giá trị từ object một cách an toàn**

Giả sử bạn muốn viết một hàm lấy giá trị từ object nhưng đảm bảo key hợp lệ:

```ts
tsCopyEditfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person = { name: "Alice", age: 30 };

console.log(getProperty(person, "name")); // Output: Alice
console.log(getProperty(person, "age"));  // Output: 30
// console.log(getProperty(person, "email")); // ❌ Lỗi vì "email" không có trong person
```

🔹 **Giải thích:**

* `T` là kiểu của object (`person`).
* `K extends keyof T` đảm bảo `K` chỉ có thể là một key hợp lệ của `T`.
* `T[K]` là kiểu của giá trị tương ứng với key đó.

***

#### **3. `extends keyof` trong Interface và Class**

Bạn có thể dùng `extends keyof` với interface hoặc class.

**Ví dụ 2: Interface với Generics**

```ts
tsCopyEditinterface DataStorage<T> {
    data: T;
    get<K extends keyof T>(key: K): T[K];
}

const storage: DataStorage<{ id: number; name: string }> = {
    data: { id: 1, name: "Alice" },
    get(key) {
        return this.data[key];
    }
};

console.log(storage.get("id"));   // Output: 1
console.log(storage.get("name")); // Output: Alice
// console.log(storage.get("email")); // ❌ Lỗi vì "email" không có trong data
```

🔹 **Giải thích:**

* `T` là kiểu dữ liệu của object.
* `K extends keyof T` đảm bảo chỉ truy cập vào các key hợp lệ.
* `T[K]` là kiểu dữ liệu của key đó.

***

#### **4. Cập nhật giá trị trong object với `extends keyof`**

Bạn có thể sử dụng `extends keyof` để tạo một hàm cập nhật giá trị của object một cách an toàn.

**Ví dụ 3: Hàm cập nhật thuộc tính**

```ts
tsCopyEditfunction updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
    obj[key] = value;
}

const user = { username: "Alice", age: 25 };

updateProperty(user, "username", "Bob"); // ✅ Hợp lệ
updateProperty(user, "age", 30);         // ✅ Hợp lệ
// updateProperty(user, "email", "bob@example.com"); // ❌ Lỗi vì "email" không tồn tại

console.log(user); // Output: { username: "Bob", age: 30 }
```

🔹 **Giải thích:**

* `T` là kiểu của object.
* `K extends keyof T` đảm bảo `K` là một key hợp lệ.
* `value: T[K]` đảm bảo giá trị được truyền vào đúng kiểu dữ liệu.

***

### **Tóm tắt**

| Cú pháp             | Ý nghĩa                                          |
| ------------------- | ------------------------------------------------ |
| `keyof T`           | Lấy danh sách key của object `T`                 |
| `K extends keyof T` | `K` phải là một key hợp lệ của `T`               |
| `T[K]`              | Kiểu dữ liệu của thuộc tính `K` trong object `T` |

#### **Lợi ích của `extends keyof`**

✅ Giúp **tránh lỗi truy cập key không hợp lệ**\
✅ Hỗ trợ **code an toàn hơn** với **type checking**\
✅ Giúp viết **hàm tái sử dụng tốt hơn** với Generics

Bạn đã hiểu rõ hơn chưa? Nếu cần thêm ví dụ thực tế hơn, mình có thể giúp nhé! 🚀
