第4章:泛型入门——泛型函数、泛型接口、泛型约束、keyof操作符
好,咱们进入泛型的世界。
说实话,我刚学 TypeScript 时,对泛型有点发怵。觉得这东西太抽象,又是尖括号又是类型参数的,看着就头疼。但后来在项目中用多了,我发现泛型其实特别实在——它解决了一个核心问题:写一次,适配多种类型。
你想想看,如果没有泛型,我们可能要为每种数据类型都写一遍相同的逻辑。那代码得多冗余?
4.1 泛型函数:让函数更灵活
先从一个最简单的场景说起。
假设你要写一个函数,接收一个参数,原样返回。这个函数叫 identity。如果只用 any,类型信息就丢了。用联合类型?那得列多少种?
泛型函数就是干这个的。
function identity<T>(arg: T): T {
return arg;
}
// 使用
const num = identity<number>(42); // number 类型
const str = identity<string>('hello'); // string 类型
这里的 <T> 就是类型参数。它像一个占位符,调用时再指定具体类型。
我个人习惯把类型参数命名为 T、U、V,简单明了。当然,如果语义更复杂,也可以用 Item、Key、Value 这样的名字。
identity('hello'),它会自动推断出 T 是 string,不用你手动写尖括号。
我在项目中遇到过这样一个场景:后端返回的数据结构不确定,但前端需要统一处理。用泛型函数包装一下,调用时传入具体类型,既安全又灵活。
4.2 泛型接口:定义可复用的类型结构
泛型不光能用在函数上,接口也可以。
举个例子,我们经常需要定义 API 响应的结构。如果每个接口都写一遍 code、message、data,那太累了。用泛型接口,一次定义,到处复用。
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 用户接口
interface User {
id: number;
name: string;
}
// 使用
const userResponse: ApiResponse<User> = {
code: 200,
message: 'success',
data: { id: 1, name: '张三' }
};
你看,ApiResponse<T> 就像一个模板。传入 User,data 就是 User 类型;传入 string[],data 就是数组。
嗯,这里要注意:泛型接口的 T 可以设置默认值。比如 interface ApiResponse<T = any>,这样不指定类型时也不会报错。
4.3 泛型约束:别让类型太自由
泛型虽然灵活,但也不能太放飞自我。有时候我们需要对类型参数加一些限制。
比如,你想写一个函数,获取对象的 length 属性。但并不是所有类型都有 length。如果传入一个数字,运行时就会出错。
这时候就需要泛型约束。
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength('hello'); // 5
logLength([1, 2, 3]); // 3
// logLength(123); // 报错!number 没有 length
<T extends HasLength> 的意思是:T 必须满足 HasLength 这个形状。说白了,就是给泛型加了个「门槛」。
我曾经在项目中犯过一个错:写了一个泛型函数,没加约束,结果传入一个 null,运行时直接崩溃。后来加了约束,编译阶段就拦截了。嗯,避坑指南来了——能用约束的地方,别偷懒。
length 存在,但值可能是 undefined,这需要你额外处理。
4.4 keyof 操作符:获取对象的键
keyof 是 TypeScript 里一个很实用的操作符。它返回一个类型的所有键的联合类型。
interface Person {
name: string;
age: number;
email: string;
}
type PersonKeys = keyof Person; // 'name' | 'age' | 'email'
这有什么用?配合泛型,能写出非常安全的访问函数。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: '李四', age: 30, email: 'li@example.com' };
const name = getProperty(person, 'name'); // string 类型
// const invalid = getProperty(person, 'address'); // 报错!address 不在 keyof Person 中
你看,K extends keyof T 保证了 key 一定是 obj 的合法键。如果你传了一个不存在的键,TypeScript 直接报错。
我个人觉得,keyof 是 TypeScript 类型体操的基础操作之一。很多高级类型(比如 Pick、Omit)底层都用到了它。
keyof 约束,可以避免拼写错误。我在项目中就用这个模式写过通用的表单校验函数,效果很好。
4.5 综合示例:泛型 + keyof 实现类型安全的更新函数
咱们把前面学的串起来,写一个实用的例子。
假设你有一个对象,你想更新它的某个属性。但你不能让用户随便改,得保证属性名和值的类型匹配。
function updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
const user = { id: 1, name: '王五', age: 25 };
const updatedUser = updateProperty(user, 'age', 26); // 正确
// const wrong = updateProperty(user, 'age', '26'); // 报错!age 是 number,不能传 string
这个函数虽然简单,但类型安全做得非常到位:
K extends keyof T确保key是合法属性T[K]确保value的类型与属性类型一致- 返回值也是
T类型,保持了完整性
我记得有一次,团队里有人直接写 obj[key] = value 来更新属性,结果类型不对,运行时出了 bug。后来我推荐用这种泛型函数,编译阶段就能发现问题。
4.6 避坑指南与个人经验
最后,分享几个我踩过的坑:
- 泛型别滥用。 不是所有地方都需要泛型。如果一个函数只处理一种类型,直接用具体类型就好。泛型会增加代码的复杂度。
- 约束要适度。 我曾经写过一个泛型约束,限制得太死,导致很多合法类型传不进来。后来放宽了约束,反而更灵活。
- keyof 注意联合类型。 如果对象类型是
Record<string, any>,keyof返回的是string,不是具体的键。这时候需要额外处理。 - 泛型默认值很有用。 比如
function createArray<T = string>(length: number, value: T): T[],不传类型参数时默认用string,减少调用方的负担。
嗯,泛型入门就到这里。下一章我们会深入更复杂的泛型用法,比如条件类型、映射类型。但今天的内容,已经足够你在日常项目中写出类型安全的代码了。
记住:泛型不是炫技,而是为了让代码更健壮、更可维护。多用几次,你就会爱上它。