TypeScript 泛型入门:从类型参数到约束与默认值

举报
神的孩子都在跳舞 发表于 2026/11/10 08:39:34 2026/11/10
【摘要】 在日常开发中,我们常常要写一些“形状相同、类型不同”的逻辑:取数组的第一项、交换两个变量的值、把一个对象包装成统一的响应结构。如果为每种类型各写一个函数,代码会迅速膨胀;如果全部用 any,又等于放弃了类型检查。泛型就是为了解决这个矛盾而存在的:它让类型也可以像值一样被“参数化”,在调用时才确定。 一、从一个最简单的函数说起先看最常见的入门例子 identity:function ident...

在日常开发中,我们常常要写一些“形状相同、类型不同”的逻辑:取数组的第一项、交换两个变量的值、把一个对象包装成统一的响应结构。如果为每种类型各写一个函数,代码会迅速膨胀;如果全部用 any,又等于放弃了类型检查。泛型就是为了解决这个矛盾而存在的:它让类型也可以像值一样被“参数化”,在调用时才确定。

一、从一个最简单的函数说起

先看最常见的入门例子 identity:

function identity<T>(value: T): T {
  return value;
}

const n = identity(42);        // T 被推断为 number,n 的类型是 number
const s = identity("hello");   // T 被推断为 string
const arr = identity([1, 2, 3]); // T 被推断为 number[]

尖括号里的 T 就是类型参数,习惯上用大写字母表示。调用 identity 时可以显式传入类型,也可以让编译器根据实参自动推断;大多数情况下,推断就足够了,写起来更简洁。

如果不用泛型,identity 的参数只能写成 any:

function identityAny(value: any): any {
  return value;
}

两者看起来都能跑,但返回值类型完全不同:identity(42) 返回的是 number,可以直接调用数值方法;而 identityAny(42) 返回的是 any,类型检查器不会再帮你发现任何错误。泛型的意义正在于此:让“输入什么类型,就返回什么类型”这种关系能够被类型系统表达出来。

二、泛型不止能用在函数上

类型参数同样可以出现在接口、类型别名和类里。比如一个通用的响应包装:

interface ApiResult<T> {
  code: number;
  message: string;
  data: T;
}

const userResult: ApiResult<{ name: string; age: number }> = {
  code: 0,
  message: "ok",
  data: { name: "Tom", age: 18 },
};

再比如一个键值对类型,两个类型参数各自独立:

type Pair<K, V> = {
  key: K;
  value: V;
};

const entry: Pair<string, number> = { key: "score", value: 98 };

泛型类也是同样的道理,常见的栈结构就是很好的例子:

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("a"); // 报错:string 不能赋给 number

一旦实例化时确定了 T,整个类的所有方法都会围绕这个类型工作,写错类型在编译期就会被拦下来。

三、泛型约束:给类型参数划定范围

有时候我们希望类型参数不是“任意类型”,而是“满足某些条件的任意类型”。这时就要用到约束,关键字是 extends。

最常见的场景是访问某个属性。下面的函数想取参数的 length,但如果 T 是任意类型,编译器无法保证它一定有 length:

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // 报错:number 没有 length 属性

约束并不缩小泛型的适用面,而是把“调用者需要知道的前提”写进类型里。

另一个高频用法是 keyof 约束,它可以保证第二个参数一定是第一个参数对象的合法属性名:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Tom", age: 18 };
const userName = getProperty(user, "name"); // string
// getProperty(user, "gender"); // 报错:gender 不是 user 的属性

如果不用这个约束,key 就只能写成 string,返回值也只能是 any,写错属性名要到运行期才暴露。

四、默认值:让常用类型可以省略

类型参数也可以像函数参数一样设置默认值。当调用方不传类型实参时,就使用默认值:

interface Page<T = string> {
  list: T[];
  total: number;
}

const page1: Page = { list: ["a", "b"], total: 2 };
const page2: Page<number> = { list: [1, 2], total: 2 };

在函数里同样适用:

function createArray<T = string>(length: number, value: T): T[] {
  return new Array(length).fill(value);
}

createArray(3, "x"); // string[]
createArray<number>(3, 1); // number[]

需要注意两点:第一,有默认值的类型参数必须放在参数列表的末尾,就像函数的可选参数一样;第二,默认值只是“不传时的 fallback”,显式传入时仍然以传入的为准。

五、使用泛型时的几个提醒

第一,泛型不是 any 的替代品。写出尖括号里的 T 却不在任何地方使用它,或者上来就把所有参数写成泛型,都是没有意义的。类型参数应当真实参与到参数类型、返回值类型或成员类型里。

第二,类型参数的名字可以更表意。单字母 T、K、V、E 是社区约定俗成的做法,但在复杂场景下,用 Type、Key、Value 这样的完整单词会让代码更易读。

第三,不要过度设计。只在一个地方用到的简单逻辑,没必要为了“通用”硬套泛型;当第二、第三个相似类型出现时,再把它们抽象成泛型,往往是更合适的时机。

小结

泛型的学习路径可以概括为三步:先用类型参数把“类型当作参数”;再用 extends 约束为类型参数划定范围;最后用默认值减少重复书写。掌握这三步之后,像工具类型、条件类型这些进阶内容,都会建立在相同的思想之上。建议从自己项目里的重复函数入手,把它们改写成泛型版本,写几遍之后,泛型就不再是陌生的语法,而是日常工具箱里顺手的一件工具。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。