一、概述
泛型是 TypeScript 中的一项强大功能,允许你在定义函数、类、接口和类型别名时,不必指定具体的类型,而是在使用时再指定具体的类型。这样可以提高代码的灵活性和可重用性,同时保持类型安全。本文将通过生活中的类比来解释泛型的概念,并展示其在实际开发中的使用场景及代码示例。
二、函数泛型
泛型函数是在函数定义时使用泛型参数,可以在函数调用时指定具体的类型。我们可以把泛型函数类比成一个多功能容器,它可以装任何类型的物品。
生活中的类比
想象一个多功能容器,可以装不同种类的物品:水果、文具、衣物等。你可以在需要时将具体的物品放入容器,而不需要为每种物品准备不同的容器。
示例
function identity<T>(arg: T): T {return arg;}let output1 = identity<string>("myString"); // 容器里放了字符串let output2 = identity<number>(100); // 容器里放了数字
使用场景
- 统一接口:当函数的输入类型与返回类型相同时,如获取输入值并返回。
 
function echo<T>(value: T): T {return value;}console.log(echo<string>("Hello World")); // Hello Worldconsole.log(echo<number>(123)); // 123
- 多样数据处理:当函数需要处理多种类型的数据,如一个可以打印任意类型数据的函数。
 
function logValue<T>(value: T): void {console.log(value);}logValue<string>("This is a string");logValue<number>(42);logValue<boolean>(true);
三、接口泛型
接口泛型允许你在定义接口时使用泛型参数,以提高接口的灵活性。可以类比为一个多用途工具箱,可以包含不同类型的工具。
生活中的类比
一个多用途工具箱,可以装不同种类的工具:锤子、螺丝刀、扳手等。你可以根据需要放入不同的工具,而不需要为每种工具准备不同的工具箱。
示例
interface GenericIdentityFn<T> {(arg: T): T;}function identity<T>(arg: T): T {return arg;}let myIdentity: GenericIdentityFn<number> = identity; // 工具箱里放了数字处理工具
使用场景
- 通用接口:当接口定义的结构需要处理多种类型时,如一个处理不同类型数据的函数接口。
 
interface GenericStorage<T> {getItem: (key: string) => T;setItem: (key: string, value: T) => void;}class LocalStorage<T> implements GenericStorage<T> {private storage: { [key: string]: T } = {};getItem(key: string): T {return this.storage[key];}setItem(key: string, value: T): void {this.storage[key] = value;}}const stringStorage = new LocalStorage<string>();stringStorage.setItem("name", "Alice");console.log(stringStorage.getItem("name")); // Aliceconst numberStorage = new LocalStorage<number>();numberStorage.setItem("age", 30);console.log(numberStorage.getItem("age")); // 30
- 接口复用:当接口需要在多个地方复用时,如多个不同类型的数据处理函数都遵循同一个接口。
 
interface Processor<T> {process: (input: T) => T;}function stringProcessor(input: string): string {return input.toUpperCase();}function numberProcessor(input: number): number {return input * input;}let myStringProcessor: Processor<string> = { process: stringProcessor };let myNumberProcessor: Processor<number> = { process: numberProcessor };console.log(myStringProcessor.process("hello")); // HELLOconsole.log(myNumberProcessor.process(5)); // 25
四、类泛型
类泛型允许你在定义类时使用泛型参数,以提高类的灵活性。可以类比为一个万能的收纳箱,可以存放不同种类的物品。
生活中的类比
一个万能的收纳箱,可以存放不同种类的物品:衣服、书籍、玩具等。你可以根据需要将不同的物品放入收纳箱,而不需要为每种物品准备不同的收纳箱。
示例
class GenericBox<T> {content: T;constructor(content: T) {this.content = content;}getContent(): T {return this.content;}}let stringBox = new GenericBox<string>("Hello, world!"); // 收纳箱里放了字符串let numberBox = new GenericBox<number>(123); // 收纳箱里放了数字
使用场景
- 多用途类:当类需要处理多种类型的数据时,如一个可以存放不同类型数据的容器类。
 
class StorageBox<T> {private _items: T[] = [];addItem(item: T): void {this._items.push(item);}getItems(): T[] {return this._items;}}const bookBox = new StorageBox<string>();bookBox.addItem("TypeScript Handbook");console.log(bookBox.getItems()); // ["TypeScript Handbook"]const numberBox = new StorageBox<number>();numberBox.addItem(42);console.log(numberBox.getItems()); // [42]
- 类成员复用:当类的某些成员或方法需要在多个地方复用时,如一个可以返回任意类型数据的成员方法。
 
class DataContainer<T> {private _data: T;constructor(data: T) {this._data = data;}getData(): T {return this._data;}setData(data: T): void {this._data = data;}}const stringContainer = new DataContainer<string>("Initial String");console.log(stringContainer.getData()); // Initial Stringconst numberContainer = new DataContainer<number>(100);console.log(numberContainer.getData()); // 100
五、泛型约束
有时我们希望泛型参数满足某些条件,这时可以使用泛型约束。可以类比为一种特定类型的容器,只能装某种特定类型的物品。
生活中的类比
一个专门用于存放有长度特性的容器,如笔盒只能装有长度的物品:铅笔、尺子等。
示例
interface Lengthwise {length: number;}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 只能装有长度属性的物品return arg;}loggingIdentity({ length: 10, value: 3 }); // 可以装有长度属性的对象
使用场景
- 特定属性要求:当泛型参数需要具有某些特定属性或方法时,如只能处理具有长度属性的数据。
 
function printLength<T extends { length: number }>(item: T): void {console.log(item.length);}printLength("Hello, world!"); // 13printLength([1, 2, 3, 4, 5]); // 5printLength({ length: 10, value: 100 }); // 10
- 类型兼容:当泛型参数需要与其他类型进行兼容时,如处理特定接口类型的数据。
 
interface Name {name: string;}function greet<T extends Name>(person: T): void {console.log(`Hello, ${person.name}`);}greet({ name: "Alice" }); // Hello, Alicegreet({ name: "Bob", age: 25 }); // Hello, Bob
六、多重泛型
有时我们需要在一个函数或类中使用多个泛型参数,这时可以使用多重泛型。可以类比为一个可以同时处理多种类型的容器。
生活中的类比
一个可以同时存放多种物品的储物柜,可以同时存放衣服和鞋子,互不干扰。
示例
function merge<T, U>(obj1: T, obj2: U): T & U {let result = {} as T & U;for (let key in obj1) {result[key] = obj1[key];}for (let key in obj2) {result[key] = obj2[key];}return result;}let mergedObj = merge({ name: "John" }, { age: 30 }); // 储物柜里同时存放了姓名和年龄
使用场景
- 多类型处理:当函数或类需要处理多个不同类型的参数时,如一个可以合并不同类型对象的函数。
 
function combine<T, U>(a: T, b: U):T & U {return { ...a, ...b };}const combined = combine({ firstName: "John" }, { lastName: "Doe" });console.log(combined); // { firstName: "John", lastName: "Doe" }
- 数据结构合并:当需要合并多个对象或数据结构时,如将多个数据对象合并为一个。
 
interface Address {street: string;city: string;}interface Contact {phone: string;email: string;}function mergeInfo<T, U>(info1: T, info2: U): T & U {return { ...info1, ...info2 };}const fullInfo = mergeInfo({ street: "123 Main St", city: "Anytown" }, { phone: "123-456-7890", email: "example@example.com" });console.log(fullInfo); // { street: "123 Main St", city: "Anytown", phone: "123-456-7890", email: "example@example.com" }
七、泛型工具类型
TypeScript 提供了一些内置的工具类型,用于操作和变换泛型类型。可以类比为一些通用工具,可以对数据进行不同方式的操作。
生活中的类比
一套通用的工具,可以对物品进行各种处理:剪刀可以裁剪,胶水可以粘合,封箱带可以封装等。
示例
interface Person {name: string;age: number;}type ReadonlyPerson = Readonly<Person>; // 把所有属性变为只读type PartialPerson = Partial<Person>; // 把所有属性变为可选type PickName = Pick<Person, "name">; // 只选择特定的属性
使用场景
- 类型变换:当需要对类型进行变换或扩展时,如将某个类型的所有属性变为只读。
 
interface Todo {title: string;description: string;}const todo: Readonly<Todo> = {title: "Learn TypeScript",description: "Understand advanced types"};// todo.title = "Learn JavaScript"; // Error: Cannot assign to 'title' because it is a read-only property.
- 创建新类型:当需要创建新的类型以提高代码的可读性和可维护性时,如只选择某个类型的部分属性。
 
interface Task {id: number;title: string;completed: boolean;}type TaskPreview = Pick<Task, "id" | "title">;const task: TaskPreview = {id: 1,title: "Study TypeScript"};console.log(task); // { id: 1, title: "Study TypeScript" }
八、泛型类型别名
类型别名使用 type 关键字来创建,可以用于为复杂的类型表达式定义一个新的名称。泛型类型别名则是在类型别名中使用泛型参数。可以类比为一个标签,用来标识某种特定的物品。
生活中的类比
一个标签,可以标识不同种类的物品:水果标签、文具标签、衣物标签等。你可以根据需要给不同物品贴上标签,以便识别和管理。
示例
type Identity<T> = (arg: T) => T;const identityString: Identity<string> = (arg) => arg; // 标签标识字符串处理函数const identityNumber: Identity<number> = (arg) => arg; // 标签标识数字处理函数
使用场景
- 重用复杂类型定义:当某个类型表达式非常复杂时,可以通过类型别名来简化代码。
 
type Callback<T> = (data: T) => void;function fetchData<T>(url: string, callback: Callback<T>): void {// 模拟异步数据获取setTimeout(() => {const data = JSON.parse('{"name": "Alice"}') as T;callback(data);}, 1000);}fetchData<{ name: string }>("https://api.example.com/user", (data) => {console.log(data.name); // Alice});
- 提高代码可读性:为复杂类型定义一个易记的名称,可以使代码更加清晰易读。
 
type User = {id: number;name: string;};type UserId = User["id"];type UserName = User["name"];const userId: UserId = 123;const userName: UserName = "Alice";console.log(userId, userName); // 123 Alice
- 灵活定义泛型接口:在需要定义多个泛型类型时,使用类型别名可以使代码更简洁。
 
type Pair<T, U> = [T, U];const pair1: Pair<string, number> = ["Alice", 30];const pair2: Pair<boolean, string> = [true, "Success"];console.log(pair1); // ["Alice", 30]console.log(pair2); // [true, "Success"]
九、结合接口使用
类型别名还可以与接口结合使用,以创建更灵活的类型定义。
interface User {name: string;age: number;}type ReadonlyUser = Readonly<User>;type PartialUser = Partial<User>;const readonlyUser: ReadonlyUser = { name: "Alice", age: 25 };// readonlyUser.age = 26; // Error: Cannot assign to 'age' because it is a read-only property.const partialUser: PartialUser = { name: "Bob" }; // age is optional
十、结合联合类型和交叉类型
泛型类型别名可以与联合类型和交叉类型结合使用,创建更复杂的类型定义。
type Result<T> = { success: true; value: T } | { success: false; error: string };const successResult: Result<number> = { success: true, value: 42 };const errorResult: Result<number> = { success: false, error: "Something went wrong" };
在上面的例子中,类型别名 Result 使用了泛型 T 和联合类型,表示一个可能成功或失败的结果类型。
十一、结论
泛型是 TypeScript 中的重要特性,提供了极大的灵活性和类型安全性。通过掌握泛型函数、接口泛型、类泛型、泛型约束、多重泛型、泛型工具类型和泛型类型别名,你可以编写出更具通用性和可重用性的代码。希望这篇文章能够帮助你更好地理解和使用 TypeScript 的泛型。
