泛型基础:让函数「通吃」多种类型

本节目标

没有泛型时,想写「返回第一个元素」会丢失类型信息:

// 运行环境:Node.js 16+ + typescript;类型检查 npx tsc --noEmit ts-l9.ts

// 不用泛型:返回 any,调用方拿不到元素真实类型,TS 保护失效
function firstBad(arr: any[]): any {
  return arr[0]
}

// 用泛型 <T>:T 是「占位类型」,调用时由实参推断
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}
const a = first([1, 2, 3])      // T 推断为 number,a: number | undefined
const b = first(['x', 'y'])     // T 推断为 string
const c = first([{ id: 1 }])    // T 推断为 { id: number }
console.log(a, b, c)

泛型也能用在 interface / type 上,做出「参数化的容器」:

// 一个「可能有值、可能加载中、可能出错」的通用状态
interface RemoteData<T> {
  loading: boolean
  data: T | null
  error: string | null
}
const userState: RemoteData<{ name: string }> = {
  loading: false,
  data: { name: 'Tom' },
  error: null,
}

一句话理解泛型:它让类型「也变成参数」。就像函数参数让「值」可变,泛型让「类型」可变——于是你写一次逻辑,能安全服务无数种类型。

名词解释

泛型(Generics):把「类型」也变成函数/接口的参数。写法 <T> 里的 T 是个占位符(Type 的缩写),调用时才被具体类型替换。它解决的问题是:既要「复用同一段逻辑」,又要「保留每种调用具体的类型信息」。没有它,你只能退回 any,丢掉全部类型安全。

类型参数(Type Parameter):<T>、<T, U> 里的 T、U 就叫类型参数。约定俗成 T=第一个、U=第二个、K=键、V=值、R=返回值。它们和函数参数一样,本质是把「未知」推迟到调用时决定。

课后练习

练习 1:写一个泛型函数 last<T>(arr: T[]): T | undefined 返回最后一个元素。

答案:

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1]
}

练习 2:为什么 first([1,2,3]) 返回类型不是 number 而是 number | undefined?

答案:因为空数组 [] 也合法,arr[0] 可能取不到,所以 TS 诚实地把「可能不存在」也编进类型。这恰恰是 TS 比人靠谱的地方——它逼你想「数组为空怎么办」。

总结

泛型是 TS 从「能写」跨到「写得好」的分水岭。我的观点:新手不要被 <T> 吓到,它本质就是「类型的参数」——你早就会函数的 x 参数了,T 只是把同一个思想搬到类型维度。最大的收益是:泛型函数既通用又不丢类型。以前为了复用只能写 any,调用方得到一堆 any 等于自废武功;泛型让你「一次编写,处处保留具体类型」。建议从 first/last/identity 这种小工具练手,体会「T 由调用方决定」的感觉,再去看标准库 Array.map 的签名就会豁然开朗。