一、概述

泛型是 TypeScript 中的一项强大功能,允许你在定义函数、类、接口和类型别名时,不必指定具体的类型,而是在使用时再指定具体的类型。这样可以提高代码的灵活性和可重用性,同时保持类型安全。本文将通过生活中的类比来解释泛型的概念,并展示其在实际开发中的使用场景及代码示例。

二、函数泛型

泛型函数是在函数定义时使用泛型参数,可以在函数调用时指定具体的类型。我们可以把泛型函数类比成一个多功能容器,它可以装任何类型的物品。

生活中的类比

想象一个多功能容器,可以装不同种类的物品:水果、文具、衣物等。你可以在需要时将具体的物品放入容器,而不需要为每种物品准备不同的容器。

示例

  1. function identity<T>(arg: T): T {
  2. return arg;
  3. }
  4. let output1 = identity<string>("myString"); // 容器里放了字符串
  5. let output2 = identity<number>(100); // 容器里放了数字

使用场景

  • 统一接口:当函数的输入类型与返回类型相同时,如获取输入值并返回。
  1. function echo<T>(value: T): T {
  2. return value;
  3. }
  4. console.log(echo<string>("Hello World")); // Hello World
  5. console.log(echo<number>(123)); // 123
  • 多样数据处理:当函数需要处理多种类型的数据,如一个可以打印任意类型数据的函数。
  1. function logValue<T>(value: T): void {
  2. console.log(value);
  3. }
  4. logValue<string>("This is a string");
  5. logValue<number>(42);
  6. logValue<boolean>(true);

三、接口泛型

接口泛型允许你在定义接口时使用泛型参数,以提高接口的灵活性。可以类比为一个多用途工具箱,可以包含不同类型的工具。

生活中的类比

一个多用途工具箱,可以装不同种类的工具:锤子、螺丝刀、扳手等。你可以根据需要放入不同的工具,而不需要为每种工具准备不同的工具箱。

示例

  1. interface GenericIdentityFn<T> {
  2. (arg: T): T;
  3. }
  4. function identity<T>(arg: T): T {
  5. return arg;
  6. }
  7. let myIdentity: GenericIdentityFn<number> = identity; // 工具箱里放了数字处理工具

使用场景

  • 通用接口:当接口定义的结构需要处理多种类型时,如一个处理不同类型数据的函数接口。
  1. interface GenericStorage<T> {
  2. getItem: (key: string) => T;
  3. setItem: (key: string, value: T) => void;
  4. }
  5. class LocalStorage<T> implements GenericStorage<T> {
  6. private storage: { [key: string]: T } = {};
  7. getItem(key: string): T {
  8. return this.storage[key];
  9. }
  10. setItem(key: string, value: T): void {
  11. this.storage[key] = value;
  12. }
  13. }
  14. const stringStorage = new LocalStorage<string>();
  15. stringStorage.setItem("name", "Alice");
  16. console.log(stringStorage.getItem("name")); // Alice
  17. const numberStorage = new LocalStorage<number>();
  18. numberStorage.setItem("age", 30);
  19. console.log(numberStorage.getItem("age")); // 30
  • 接口复用:当接口需要在多个地方复用时,如多个不同类型的数据处理函数都遵循同一个接口。
  1. interface Processor<T> {
  2. process: (input: T) => T;
  3. }
  4. function stringProcessor(input: string): string {
  5. return input.toUpperCase();
  6. }
  7. function numberProcessor(input: number): number {
  8. return input * input;
  9. }
  10. let myStringProcessor: Processor<string> = { process: stringProcessor };
  11. let myNumberProcessor: Processor<number> = { process: numberProcessor };
  12. console.log(myStringProcessor.process("hello")); // HELLO
  13. console.log(myNumberProcessor.process(5)); // 25

四、类泛型

类泛型允许你在定义类时使用泛型参数,以提高类的灵活性。可以类比为一个万能的收纳箱,可以存放不同种类的物品。

生活中的类比

一个万能的收纳箱,可以存放不同种类的物品:衣服、书籍、玩具等。你可以根据需要将不同的物品放入收纳箱,而不需要为每种物品准备不同的收纳箱。

示例

  1. class GenericBox<T> {
  2. content: T;
  3. constructor(content: T) {
  4. this.content = content;
  5. }
  6. getContent(): T {
  7. return this.content;
  8. }
  9. }
  10. let stringBox = new GenericBox<string>("Hello, world!"); // 收纳箱里放了字符串
  11. let numberBox = new GenericBox<number>(123); // 收纳箱里放了数字

使用场景

  • 多用途类:当类需要处理多种类型的数据时,如一个可以存放不同类型数据的容器类。
  1. class StorageBox<T> {
  2. private _items: T[] = [];
  3. addItem(item: T): void {
  4. this._items.push(item);
  5. }
  6. getItems(): T[] {
  7. return this._items;
  8. }
  9. }
  10. const bookBox = new StorageBox<string>();
  11. bookBox.addItem("TypeScript Handbook");
  12. console.log(bookBox.getItems()); // ["TypeScript Handbook"]
  13. const numberBox = new StorageBox<number>();
  14. numberBox.addItem(42);
  15. console.log(numberBox.getItems()); // [42]
  • 类成员复用:当类的某些成员或方法需要在多个地方复用时,如一个可以返回任意类型数据的成员方法。
  1. class DataContainer<T> {
  2. private _data: T;
  3. constructor(data: T) {
  4. this._data = data;
  5. }
  6. getData(): T {
  7. return this._data;
  8. }
  9. setData(data: T): void {
  10. this._data = data;
  11. }
  12. }
  13. const stringContainer = new DataContainer<string>("Initial String");
  14. console.log(stringContainer.getData()); // Initial String
  15. const numberContainer = new DataContainer<number>(100);
  16. console.log(numberContainer.getData()); // 100

五、泛型约束

有时我们希望泛型参数满足某些条件,这时可以使用泛型约束。可以类比为一种特定类型的容器,只能装某种特定类型的物品。

生活中的类比

一个专门用于存放有长度特性的容器,如笔盒只能装有长度的物品:铅笔、尺子等。

示例

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. function loggingIdentity<T extends Lengthwise>(arg: T): T {
  5. console.log(arg.length); // 只能装有长度属性的物品
  6. return arg;
  7. }
  8. loggingIdentity({ length: 10, value: 3 }); // 可以装有长度属性的对象

使用场景

  • 特定属性要求:当泛型参数需要具有某些特定属性或方法时,如只能处理具有长度属性的数据。
  1. function printLength<T extends { length: number }>(item: T): void {
  2. console.log(item.length);
  3. }
  4. printLength("Hello, world!"); // 13
  5. printLength([1, 2, 3, 4, 5]); // 5
  6. printLength({ length: 10, value: 100 }); // 10
  • 类型兼容:当泛型参数需要与其他类型进行兼容时,如处理特定接口类型的数据。
  1. interface Name {
  2. name: string;
  3. }
  4. function greet<T extends Name>(person: T): void {
  5. console.log(`Hello, ${person.name}`);
  6. }
  7. greet({ name: "Alice" }); // Hello, Alice
  8. greet({ name: "Bob", age: 25 }); // Hello, Bob

六、多重泛型

有时我们需要在一个函数或类中使用多个泛型参数,这时可以使用多重泛型。可以类比为一个可以同时处理多种类型的容器。

生活中的类比

一个可以同时存放多种物品的储物柜,可以同时存放衣服和鞋子,互不干扰。

示例

  1. function merge<T, U>(obj1: T, obj2: U): T & U {
  2. let result = {} as T & U;
  3. for (let key in obj1) {
  4. result[key] = obj1[key];
  5. }
  6. for (let key in obj2) {
  7. result[key] = obj2[key];
  8. }
  9. return result;
  10. }
  11. let mergedObj = merge({ name: "John" }, { age: 30 }); // 储物柜里同时存放了姓名和年龄

使用场景

  • 多类型处理:当函数或类需要处理多个不同类型的参数时,如一个可以合并不同类型对象的函数。
  1. function combine<T, U>(a: T, b: U):
  2. T & U {
  3. return { ...a, ...b };
  4. }
  5. const combined = combine({ firstName: "John" }, { lastName: "Doe" });
  6. console.log(combined); // { firstName: "John", lastName: "Doe" }
  • 数据结构合并:当需要合并多个对象或数据结构时,如将多个数据对象合并为一个。
  1. interface Address {
  2. street: string;
  3. city: string;
  4. }
  5. interface Contact {
  6. phone: string;
  7. email: string;
  8. }
  9. function mergeInfo<T, U>(info1: T, info2: U): T & U {
  10. return { ...info1, ...info2 };
  11. }
  12. const fullInfo = mergeInfo({ street: "123 Main St", city: "Anytown" }, { phone: "123-456-7890", email: "example@example.com" });
  13. console.log(fullInfo); // { street: "123 Main St", city: "Anytown", phone: "123-456-7890", email: "example@example.com" }

七、泛型工具类型

TypeScript 提供了一些内置的工具类型,用于操作和变换泛型类型。可以类比为一些通用工具,可以对数据进行不同方式的操作。

生活中的类比

一套通用的工具,可以对物品进行各种处理:剪刀可以裁剪,胶水可以粘合,封箱带可以封装等。

示例

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. type ReadonlyPerson = Readonly<Person>; // 把所有属性变为只读
  6. type PartialPerson = Partial<Person>; // 把所有属性变为可选
  7. type PickName = Pick<Person, "name">; // 只选择特定的属性

使用场景

  • 类型变换:当需要对类型进行变换或扩展时,如将某个类型的所有属性变为只读。
  1. interface Todo {
  2. title: string;
  3. description: string;
  4. }
  5. const todo: Readonly<Todo> = {
  6. title: "Learn TypeScript",
  7. description: "Understand advanced types"
  8. };
  9. // todo.title = "Learn JavaScript"; // Error: Cannot assign to 'title' because it is a read-only property.
  • 创建新类型:当需要创建新的类型以提高代码的可读性和可维护性时,如只选择某个类型的部分属性。
  1. interface Task {
  2. id: number;
  3. title: string;
  4. completed: boolean;
  5. }
  6. type TaskPreview = Pick<Task, "id" | "title">;
  7. const task: TaskPreview = {
  8. id: 1,
  9. title: "Study TypeScript"
  10. };
  11. console.log(task); // { id: 1, title: "Study TypeScript" }

八、泛型类型别名

类型别名使用 type 关键字来创建,可以用于为复杂的类型表达式定义一个新的名称。泛型类型别名则是在类型别名中使用泛型参数。可以类比为一个标签,用来标识某种特定的物品。

生活中的类比

一个标签,可以标识不同种类的物品:水果标签、文具标签、衣物标签等。你可以根据需要给不同物品贴上标签,以便识别和管理。

示例

  1. type Identity<T> = (arg: T) => T;
  2. const identityString: Identity<string> = (arg) => arg; // 标签标识字符串处理函数
  3. const identityNumber: Identity<number> = (arg) => arg; // 标签标识数字处理函数

使用场景

  • 重用复杂类型定义:当某个类型表达式非常复杂时,可以通过类型别名来简化代码。
  1. type Callback<T> = (data: T) => void;
  2. function fetchData<T>(url: string, callback: Callback<T>): void {
  3. // 模拟异步数据获取
  4. setTimeout(() => {
  5. const data = JSON.parse('{"name": "Alice"}') as T;
  6. callback(data);
  7. }, 1000);
  8. }
  9. fetchData<{ name: string }>("https://api.example.com/user", (data) => {
  10. console.log(data.name); // Alice
  11. });
  • 提高代码可读性:为复杂类型定义一个易记的名称,可以使代码更加清晰易读。
  1. type User = {
  2. id: number;
  3. name: string;
  4. };
  5. type UserId = User["id"];
  6. type UserName = User["name"];
  7. const userId: UserId = 123;
  8. const userName: UserName = "Alice";
  9. console.log(userId, userName); // 123 Alice
  • 灵活定义泛型接口:在需要定义多个泛型类型时,使用类型别名可以使代码更简洁。
  1. type Pair<T, U> = [T, U];
  2. const pair1: Pair<string, number> = ["Alice", 30];
  3. const pair2: Pair<boolean, string> = [true, "Success"];
  4. console.log(pair1); // ["Alice", 30]
  5. console.log(pair2); // [true, "Success"]

九、结合接口使用

类型别名还可以与接口结合使用,以创建更灵活的类型定义。

  1. interface User {
  2. name: string;
  3. age: number;
  4. }
  5. type ReadonlyUser = Readonly<User>;
  6. type PartialUser = Partial<User>;
  7. const readonlyUser: ReadonlyUser = { name: "Alice", age: 25 };
  8. // readonlyUser.age = 26; // Error: Cannot assign to 'age' because it is a read-only property.
  9. const partialUser: PartialUser = { name: "Bob" }; // age is optional

十、结合联合类型和交叉类型

泛型类型别名可以与联合类型和交叉类型结合使用,创建更复杂的类型定义。

  1. type Result<T> = { success: true; value: T } | { success: false; error: string };
  2. const successResult: Result<number> = { success: true, value: 42 };
  3. const errorResult: Result<number> = { success: false, error: "Something went wrong" };

在上面的例子中,类型别名 Result 使用了泛型 T 和联合类型,表示一个可能成功或失败的结果类型。

十一、结论

泛型是 TypeScript 中的重要特性,提供了极大的灵活性和类型安全性。通过掌握泛型函数、接口泛型、类泛型、泛型约束、多重泛型、泛型工具类型和泛型类型别名,你可以编写出更具通用性和可重用性的代码。希望这篇文章能够帮助你更好地理解和使用 TypeScript 的泛型。