泛型是 TypeScript 类型系统中一个非常强大和核心的特性,它允许我们编写可重用、灵活且类型安全的组件(如函数、类、接口、类型别名)。
一、 什么是泛型?为什么需要它?
想象一下,你想编写一个函数,它接收一个参数并返回该参数。
1. 不用泛型的问题:
-
使用 any 类型:
function identityAny(arg: any): any { return arg; } let outputAny = identityAny("myString"); // outputAny 的类型是 any // 问题:丢失了类型信息,编译器无法提供类型检查和智能提示 // console.log(outputAny.toFixed()); // 运行时会报错,但编译时不会提示!使用 any 会丢失类型信息,降低了类型安全性。
-
为每种类型编写特定函数:
function identityString(arg: string): string { return arg; } function identityNumber(arg: number): number { return arg; } // ...等等这会导致大量重复代码,难以维护。
2. 泛型的解决方案:
泛型允许我们编写一个“模板”函数(或类、接口),其中的类型是参数化的。你可以把它想象成给类型设置了一个占位符或变量,在使用时再指定具体的类型(或者让 TypeScript 自动推断)。
// 定义一个泛型函数 identity
// <T> 声明了一个类型变量 T (Type)
// arg: T 表示参数 arg 的类型是 T
// : T 表示函数返回值的类型也是 T
function identity<T>(arg: T): T {
return arg;
}
// 如何使用?
// 1. 显式指定类型:
let outputString = identity<string>("myString"); // T 被指定为 string,outputString 类型是 string
let outputNumber = identity<number>(123); // T 被指定为 number,outputNumber 类型是 number
// 2. 利用类型推断 (更常见):编译器会根据传入的参数自动推断 T 的类型
let outputBool = identity(true); // 传入 boolean,T 被推断为 boolean,outputBool 类型是 boolean
let outputObj = identity({ name: "Alice" }); // T 被推断为 { name: string }
// 类型安全!
// console.log(outputString.toFixed()); // Error: Property 'toFixed' does not exist on type 'string'.
console.log(outputNumber.toFixed(2)); // OK
泛型的核心优势:在保证代码可重用性的同时,维持了严格的类型约束和类型安全。
二、 泛型的基本语法和应用
泛型使用尖括号 <> 来声明类型参数 (Type Parameters) ,通常使用单个大写字母(如 T, U, K, V 等)作为类型参数的名称,但这只是约定,你可以使用任何合法的标识符。
1. 泛型函数 (Generic Functions)
如上例所示,类型参数列表放在函数名之后、参数列表之前。
// 接收一个数组,返回第一个元素
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
let firstNum = getFirstElement([1, 2, 3]); // T 推断为 number, firstNum 类型是 number | undefined
let firstStr = getFirstElement(["a", "b", "c"]); // T 推断为 string, firstStr 类型是 string | undefined
let firstEmpty = getFirstElement<boolean>([]); // 显式指定 T 为 boolean, firstEmpty 类型是 boolean | undefined
// 接收两个不同类型的参数
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
let myPair = pair("hello", 123); // T 推断为 string, U 推断为 number, myPair 类型是 [string, number]
2. 泛型接口 (Generic Interfaces)
接口也可以是泛型的,类型参数用于接口内部的成员类型。
// 定义一个泛型接口,表示键值对
interface KeyValuePair<K, V> {
key: K;
value: V;
}
let record1: KeyValuePair<string, number> = { key: "age", value: 30 };
let record2: KeyValuePair<number, boolean> = { key: 1, value: true };
// 定义一个泛型接口,表示 API 响应
interface ApiResponse<TData> {
success: boolean;
data: TData;
message?: string;
}
let userResponse: ApiResponse<{ id: number; name: string }> = {
success: true,
data: { id: 1, name: "Alice" }
};
let productResponse: ApiResponse<string[]> = {
success: false,
data: [],
message: "Products not found"
};
3. 泛型类 (Generic Classes)
类也可以使用泛型,类型参数在类名之后声明。泛型参数不能用于类的静态成员。
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
getAll(): T[] {
return [...this.data]; // 返回副本以防外部修改
}
// static method cannot use the class type parameter T directly
// static staticMethod(item: T) {} // Error
}
// 创建一个存储数字的 DataStore 实例
let numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
console.log(numberStore.getAll()); // [1, 2]
// numberStore.add("hello"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
// 创建一个存储字符串的 DataStore 实例 (类型推断)
let stringStore = new DataStore<string>();
stringStore.add("apple");
stringStore.add("banana");
console.log(stringStore.getAll()); // ["apple", "banana"]
4. 泛型类型别名 (Generic Type Aliases)
类型别名也可以是泛型的。
// 定义一个可能为 null 或 undefined 的泛型类型
type Nullable<T> = T | null | undefined;
let nullableString: Nullable<string>;
nullableString = "hello";
nullableString = null;
nullableString = undefined;
// nullableString = 123; // Error
// 定义一个包含值的容器的泛型类型
type Container<T> = { value: T };
let stringContainer: Container<string> = { value: "container content" };
let numberContainer: Container<number> = { value: 42 };
三、 泛型约束 (Generic Constraints)
有时,我们希望泛型函数或类只能处理具有特定结构或能力的类型。例如,你想写一个函数,它接收一个参数并打印其 .length 属性,那么这个参数必须得有 length 属性(比如 string 或 array)。这时就需要泛型约束。
使用 extends 关键字来约束类型参数必须符合某个接口或具有某些属性。
// 定义一个接口,表示有 length 属性
interface Lengthwise {
length: number;
}
// 约束 T 必须符合 Lengthwise 接口(即必须有 number 类型的 length 属性)
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("hello world"); // OK, string 有 length 属性
logLength([1, 2, 3]); // OK, array 有 length 属性
// logLength(123); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// logLength({ name: "test" }); // Error: Argument of type '{ name: string; }' is not assignable to parameter of type 'Lengthwise'. Property 'length' is missing.
// 约束 T 必须是某个类的子类或实现了某个接口
interface Printable { print(): void; }
class Document implements Printable { print() { console.log("Printing document..."); } }
class Image implements Printable { print() { console.log("Printing image..."); } }
function printItem<T extends Printable>(item: T): void {
item.print(); // 安全调用 print 方法,因为 T 被约束了
}
printItem(new Document());
printItem(new Image());
// class Report {}
// printItem(new Report()); // Error: Argument of type 'Report' is not assignable to parameter of type 'Printable'. Type 'Report' is missing the following properties from type 'Printable': print
四、 在泛型约束中使用类型参数
一个常见的模式是,一个类型参数受另一个类型参数的约束。例如,你想写一个函数来获取对象上某个属性的值,你需要确保这个属性名确实存在于该对象上。
使用 keyof 操作符:keyof T 会产生一个由 T 类型的所有公共属性名组成的联合类型。
// K 必须是 T 对象上的一个键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
// T[K] 是索引访问类型 (Indexed Access Type),表示 obj 对象上 key 属性的值的类型
return obj[key];
}
let car = { make: "Toyota", model: "Camry", year: 2023 };
let make = getProperty(car, "make"); // T = typeof car, K = "make", 返回 string
let year = getProperty(car, "year"); // T = typeof car, K = "year", 返回 number
// let color = getProperty(car, "color"); // Error: Argument of type '"color"' is not assignable to parameter of type '"make" | "model" | "year"'.
五、 在泛型中使用类类型
有时你想引用类的构造函数类型。
// ctor 的类型是:一个构造函数,它没有参数,并且返回一个类型为 T 的实例
function create<T>(ctor: new () => T): T {
return new ctor();
}
class BeeKeeper { honey: boolean = true; }
class ZooKeeper { tigers: number = 2; }
let beeKeeper = create(BeeKeeper); // T 推断为 BeeKeeper
let zooKeeper = create(ZooKeeper); // T 推断为 ZooKeeper
console.log(beeKeeper.honey); // true
console.log(zooKeeper.tigers); // 2
六、 泛型参数默认值
可以为泛型参数指定默认类型。当用户没有指定类型(或编译器无法推断)时,将使用默认类型。
interface RequestOptions<T = any> { // 默认类型为 any (谨慎使用)
method: 'GET' | 'POST';
headers?: Record<string, string>;
body?: T;
}
let getReq: RequestOptions = { method: 'GET' }; // T 使用默认的 any
let postReq: RequestOptions<FormData> = { method: 'POST', body: new FormData() }; // T 指定为 FormData
总结:
泛型是 TypeScript 中实现代码复用和类型安全的关键工具。通过使用类型参数 ,你可以创建适用于多种类型的函数、类、接口和类型别名。泛型约束 (extends) 允许你对这些类型参数施加限制,确保它们具有必要的结构或能力。掌握泛型对于编写高质量、可维护的 TypeScript 代码至关重要。