一、概述
泛型是 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 World
console.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")); // Alice
const 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")); // HELLO
console.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 String
const 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!"); // 13
printLength([1, 2, 3, 4, 5]); // 5
printLength({ 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, Alice
greet({ 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 的泛型。