一个只能处理一种类型的函数

先看一个场景:封装一个"返回数组第一个元素"的函数。数组可能是数字也可能是字符串,于是你写了两个:

function firstNumber(arr: number[]): number | undefined {
    return arr[0];
}

function firstString(arr: string[]): string | undefined {
    return arr[0];
}

console.log(firstNumber([1, 2, 3]));  // 输出: 1
console.log(firstString(["a", "b"])); // 输出: a

逻辑一模一样,只有类型不同。用 any 虽然能合并成一个,但类型信息就丢了,返回值变成"什么都有可能",编辑器帮不上忙。

泛型:类型也当参数传

泛型就是给函数声明一个"类型参数",习惯用 T 表示。调用时传入什么类型,T 就是什么类型:

function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

console.log(first<number>([1, 2, 3]));   // 输出: 1
console.log(first(["a", "b"]));          // 输出: a,T 被自动推断为 string
// const n: number = first(["a"]); // 报错:返回 string,不能给 number

first(["a", "b"]) 里尖括号都没写——TS 能从实参推断出 T = string,所以日常写法很省事。收益在于:返回值类型是精确的,first([...]) 之后编辑器知道它是 number,补全和检查都正常工作。

约束:不是什么类型都能进

泛型默认来者不拒,但有时你需要"至少是个对象"这类前提,用 extends 加约束:

// 要求 T 至少有 length 属性
function logLen<T extends { length: number }>(v: T): T {
    console.log(v.length);
    return v;
}

logLen("hello");  // 输出: 5,字符串有 length
logLen([1, 2]);   // 输出: 2,数组有 length
// logLen(123);  // 报错:number 没有 length

约束之后,函数体内就可以放心访问 v.length——没加约束时 TS 不允许访问 T 上的任何属性。

常见的泛型形态

标准库里泛型随处可见,数组本身就是 Array<T>:

const nums: Array<number> = [1, 2, 3];

// Promise 也是泛型:尖括号里是"将来会给你什么类型"
async function load(): Promise<string> {
    return "数据";
}

// 函数参数用泛型:map 的回调返回什么,结果数组就是什么类型
const lens = ["go", "ts"].map(s => s.length);
console.log(lens); // 输出: [2, 2],推断为 number[]

接口也能带类型参数,字段类型由使用时决定:

interface Box<T> {
    content: T;
}

const boxA: Box<string> = { content: "书" };
const boxB: Box<number> = { content: 42 };
console.log(boxA.content.toUpperCase()); // 输出: 书

小结

  • 泛型把"类型"变成参数:function f<T>(v: T),一处逻辑吃遍多种类型;
  • 大多数时候 TS 能自动推断类型参数,只有推断不出时才手写 f<number>(...);
  • T extends 约束 限定类型的下限,加了约束才能访问对应属性;
  • 数组、Promise、map 回调……标准库全是泛型,认出 <T> 就读懂了一大半 API 签名。

泛型初看吓人,其实就是"类型的形参"。下次在文档里看到 Array<T>、Promise<T> 不会再发怵,自己封装工具函数时也知道什么时候该把类型抽出来当参数。