第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> 就是类型参数。它像一个占位符,调用时再指定具体类型。

我个人习惯把类型参数命名为 TUV,简单明了。当然,如果语义更复杂,也可以用 ItemKeyValue 这样的名字。

小技巧:TypeScript 能自动推断类型。比如 identity('hello'),它会自动推断出 Tstring,不用你手动写尖括号。

我在项目中遇到过这样一个场景:后端返回的数据结构不确定,但前端需要统一处理。用泛型函数包装一下,调用时传入具体类型,既安全又灵活。

4.2 泛型接口:定义可复用的类型结构

泛型不光能用在函数上,接口也可以。

举个例子,我们经常需要定义 API 响应的结构。如果每个接口都写一遍 codemessagedata,那太累了。用泛型接口,一次定义,到处复用。

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> 就像一个模板。传入 Userdata 就是 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 类型体操的基础操作之一。很多高级类型(比如 PickOmit)底层都用到了它。

实战场景:表单验证时,我们经常需要根据字段名获取值。用 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。后来我推荐用这种泛型函数,编译阶段就能发现问题。

总结一下:泛型 + keyof 的组合,能让你写出既灵活又安全的代码。它把「类型检查」从运行时提前到了编译时,这正是 TypeScript 的价值所在。

4.6 避坑指南与个人经验

最后,分享几个我踩过的坑:

  1. 泛型别滥用。 不是所有地方都需要泛型。如果一个函数只处理一种类型,直接用具体类型就好。泛型会增加代码的复杂度。
  2. 约束要适度。 我曾经写过一个泛型约束,限制得太死,导致很多合法类型传不进来。后来放宽了约束,反而更灵活。
  3. keyof 注意联合类型。 如果对象类型是 Record<string, any>keyof 返回的是 string,不是具体的键。这时候需要额外处理。
  4. 泛型默认值很有用。 比如 function createArray<T = string>(length: number, value: T): T[],不传类型参数时默认用 string,减少调用方的负担。

嗯,泛型入门就到这里。下一章我们会深入更复杂的泛型用法,比如条件类型、映射类型。但今天的内容,已经足够你在日常项目中写出类型安全的代码了。

记住:泛型不是炫技,而是为了让代码更健壮、更可维护。多用几次,你就会爱上它。