你是不是遇到过这种场景:写了一个函数,处理数字的版本写一遍,处理字符串的版本再写一遍,处理数组的又写一遍……最后代码里全是长得差不多的“双胞胎”。今天我们来学TypeScript的泛型——一个能让你写一次、处处用的“类型模板”。从此告别复制粘贴,做个体面的程序员。
前言
想象一下,你开了一家“万能快递公司”。客户要寄书,你准备书盒;要寄衣服,你准备衣服盒;要寄手机,你准备手机盒……每种物品都要单独设计盒子,累不累?
更好的做法:设计一种可调节大小的盒子,客户说寄什么,你就把盒子调成对应大小。这个“可调节的盒子”,就是泛型。
TypeScript的泛型让你在定义函数、类、接口时,先“留个空”,等用的时候再往里填具体类型。这样既保证了类型安全,又避免了重复代码。
一、泛型长啥样?一个简单的例子
先看一个没有泛型的“悲惨世界”:
// 只能处理数字
function identityNumber(arg: number): number {
return arg;
}
// 只能处理字符串
function identityString(arg: string): string {
return arg;
}
// 要处理布尔值?再写一个……
用泛型,只需要一个:
function identity<T>(arg: T): T {
return arg;
}
这里的<T>就像个“占位符”,你调用时可以指定具体类型:
let output1 = identity<string>('hello'); // 类型是 string
let output2 = identity<number>(123); // 类型是 number
但TS很聪明,能自动推断,所以通常可以省略:
let output = identity('hello'); // TS推断出T为string
二、泛型不只是“传进去又返回来”
你可以约束参数的类型关系。比如,你想让函数接收一个数组,并返回数组的第一个元素:
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const firstNumber = getFirst([1, 2, 3]); // 类型 number
const firstString = getFirst(['a', 'b']); // 类型 string
T帮我们保持了“数组元素类型”和“返回值类型”的一致性。
三、泛型约束:给“占位符”画个圈
有时候,你不能让T为所欲为。比如你想写一个函数,打印参数的length属性:
function logLength<T>(arg: T): T {
console.log(arg.length); // ❌ 报错:T可能没有length
return arg;
}
因为T可能是number、boolean,它们没有.length。这时候需要约束——告诉TS:T必须是有length属性的类型。
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length); // ✅ 安全
return arg;
}
logLength('hello'); // 字符串有length
logLength([1, 2, 3]); // 数组有length
logLength(123); // ❌ 数字没有length,报错
extends关键字在这里不是继承,而是“约束为某个类型的子集”。
四、泛型接口:把接口变成“模具”
接口也可以泛型化,比如定义一个通用的响应结构:
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 使用
type User = { name: string; age: number };
const response: ApiResponse<User> = {
code: 200,
message: 'success',
data: { name: '张三', age: 18 }
};
这样,你就能用一个接口描述所有API返回格式,只需替换T。
五、泛型类:像造模具一样造类
类同样可以泛型:
class Queue<T> {
private data: T[] = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
const numberQueue = new Queue<number>();
numberQueue.push(123);
numberQueue.push('456'); // ❌ 报错,只能放数字
六、泛型工具类型:TS内置的“变形金刚”
TS提供了一些内置的泛型工具,能帮你快速转换类型。
1. Partial<T>:把属性都变成可选
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>; // { name?: string; age?: number; }
2. Readonly<T>:把所有属性变成只读
type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age: number; }
3. Pick<T, K>:从T中挑选部分属性
type UserName = Pick<User, 'name'>; // { name: string; }
4. Omit<T, K>:从T中排除部分属性
type UserWithoutAge = Omit<User, 'age'>; // { name: string; }
还有Record<K, T>、Exclude、Extract等,遇到具体场景时再查文档即可。
七、联合类型与交叉类型:不是泛型,但常一起用
联合类型(|):这个或那个
let value: string | number;
value = 'hello'; // OK
value = 123; // OK
value = true; // ❌
联合类型适合“不确定具体是哪个,但知道是有限的几种”。
交叉类型(&):既要又要
interface Name { name: string; }
interface Age { age: number; }
type Person = Name & Age; // 同时有name和age属性
const p: Person = { name: '张三', age: 18 };
交叉类型常用来合并多个类型。
八、类型保护:让TS相信你
当你使用联合类型时,TS会限制你只能调用所有类型共有的方法。要调用特定类型的方法,需要类型保护。
function printLength(value: string | number) {
// console.log(value.length); // ❌ 报错,number没有length
if (typeof value === 'string') {
console.log(value.length); // ✅ 这里TS知道value是string
} else {
console.log(value.toFixed(2));
}
}
除了typeof,还有instanceof、in关键字、自定义类型守卫。
九、实战:用泛型写一个“万能”的缓存函数
interface Cache<T> {
get(key: string): T | undefined;
set(key: string, value: T): void;
}
function createCache<T>(): Cache<T> {
const store: Record<string, T> = {};
return {
get(key) { return store[key]; },
set(key, value) { store[key] = value; }
};
}
const stringCache = createCache<string>();
stringCache.set('name', '张三');
const name = stringCache.get('name'); // 类型是 string | undefined
const numberCache = createCache<number>();
numberCache.set('age', 18);
看,一套代码同时服务了字符串缓存和数字缓存,类型还完全安全。
十、总结:泛型是“类型编程”的起点
- 泛型就是“类型的参数”,让组件(函数、类、接口)能适应多种类型,同时保留类型关系。
- 约束用
extends限定泛型的范围。 - 泛型接口/类让数据结构通用。
- 内置工具类型(Partial、Pick等)简化常见类型转换。
- 联合类型表示“或”,交叉类型表示“且”,类型保护用来区分联合中的具体类型。
掌握泛型,你就能写出更抽象、更复用、更安全的代码。明天我们将继续TypeScript的高级主题——装饰器,看看这个类似Java注解的特性,如何在TS里玩出花样。
如果你觉得今天的“万能模具”讲得通透,点个赞让更多人看到。明天我们聊聊装饰器——那个在Angular和NestJS里无处不在的黑魔法。我们明天见!