在 TypeScript (TS) 中,泛型是一个强大且灵活的工具,用于编写具有更高可复用性和类型安全性的代码。泛型允许我们在声明时将类型作为参数传入,使函数、接口和类能在不同的数据类型下复用,而无需重新编写逻辑。
1. 泛型的基本语法
泛型的基本语法是在函数、接口、类等结构后添加尖括号 <>,并指定一个类型参数(通常用大写字母如 T 表示)。
🌰:
function identity<T>(arg: T): T {
return arg;
}
这里 T 是泛型类型,它表示一个占位符,可以是任何类型。函数 identity 接受一个参数 arg,其类型是 T,并返回相同类型的 T。当我们调用时,可以指定具体类型,如: identity<string>("Hello")。
2. 泛型在函数中的使用
在函数中,泛型可以用来定义参数和返回值的类型,从而提高代码的灵活性和可复用性。
一个经典的 🌰 是数组的泛型方法:
function logArray<T>(arr: T[]): T[] {
console.log(arr);
return arr;
}
logArray 接收一个类型为 T 数组的参数,并返回该数组,T 可以是任意类型。通过这种方式,我们可以传入 string[]、number[] 等不同类型的数组,而不需要为每种类型写单独的函数。
3. 泛型在接口中的应用
泛型在接口中使用时,可以使接口支持不同的类型数据。
🌰,在定义一个具有 value 属性和 getValue 方法的接口时,可以使用泛型来灵活指定 value 的类型:
interface Box<T> {
value: T;
getValue(): T;
}
const stringBox: Box<string> = {
value: 'Hello',
getValue() {
return this.value;
},
};
const numberBox: Box<number> = {
value: 18,
getValue() {
return this.value;
},
};
Box<T> 接口可以适用于任何类型的 value,如 string、number,甚至是自定义类型。
4. 泛型在类中的应用
泛型在类中也有广泛的应用,特别是在需要处理不同类型的集合、数据结构时。
🌰:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
Stack 类使用了泛型 T,从而可以创建不同类型的栈,如 Stack<number> 或 Stack<string>。
5. 泛型约束(Constraints)
在某些情况下,我们可能希望限制泛型参数的类型范围,可以通过泛型约束实现这一点。
🌰,使用 extends 关键字来约束泛型类型必须具有某些属性或实现某个接口:
interface LengthType {
length: number;
}
function logLength<T extends LengthType>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength 函数的泛型参数 T 被约束为具有 length 属性的类型,这意味着只能传入具有 length 属性的对象,例如数组或字符串。
6. 多个泛型参数
有时一个泛型类型参数无法满足需求,此时可以使用多个泛型参数。
🌰:
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
pair(1, 1);
pair(1, '1');
pair('1', true);
pair 函数中,T 和 U 是两个泛型参数,允许我们将任意类型的两个值组合成一个元组。
7. 泛型的默认类型
TypeScript 支持为泛型提供默认类型,这样在没有指定类型时可以使用默认值:
🌰:
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const arr = createArray(3, 5); // 推断 T 为 number
const strArr = createArray<string>(3, 'Hello'); // 显式指定 T 为 string
createArray 函数中,设置 T 默认类型 number,这样如果调用时不指定类型,T 将默认为 number。
8. 泛型工具类型(Utility Types)
TypeScript 提供了一些内置的工具类型,可以用于处理泛型类型。例如:
- Partial<T>:将类型 T 中的所有属性变成可选。
- Readonly<T>:将类型 T 中的所有属性变为只读。常用于防止对象被修改的情况。
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = Partial<User>; // { id?: number; name?: string; age?: number; }
const user1: PartialUser = { name: 'Alice' }; // 只需要部分属性
const user2: PartialUser = {}; //也可以一个都不写
const user3: Readonly<User> = { id: 1, name: 'Alice', age: 30 }; // { readonly id: number; readonly name: string; readonly age: number; }
// user3.age = 31; // 错误:因为 user 是只读的
// const user4: Readonly<User>; // 'const' declarations must be initialized. 必须初始化 const 声明。
- Record<K, T>:将键类型为 K 的所有键映射到类型 T 的值。K 必须是字符串或字符串字面量的联合类型。常用于创建对象映射或字典。
type Role = 'admin' | 'user' | 'guest';
type Permission = Record<Role, boolean>; // 表示每个角色是否具有某种权限。
const permissions: Permission = {
admin: true,
user: false,
guest: false,
};
- Pick<T, K>:从类型 T 中选择一些属性构成新的类型。
- Omit<T, K>:从类型 T 中剔除一些属性构成新的类型。
interface User {
id: number;
name: string;
age: number;
}
type UserPreview = Pick<User, 'id' | 'name'>;
const userPreview: UserPreview = { id: 1, name: 'Alice' }; // 只包含 id 和 name
type UserWithoutAge = Omit<User, "age">;
const userWithoutAge: UserWithoutAge = { id: 1, name: "Alice" }; // age 属性被去除
9. 泛型的优势与趋势
1、可复用性:泛型允许编写更通用的代码,减少重复,提高可维护性。
2、类型安全:相比于 any,泛型提供了明确的类型约束,能够在编译时捕获错误,提升代码的可靠性。
3、适用性广:泛型在函数、接口、类、工具类型中均可使用。