泛型约束、默认与条件类型入门

本节目标

约束 extends:限制 T「至少得长这样」,否则不让你传不相关的类型。

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

// 要求 T 必须有 length 属性(如 string、数组)
interface HasLength { length: number }
function logLength<T extends HasLength>(v: T): T {
  console.log(v.length)
  return v
}
logLength('hello') // string 有 length,OK
logLength([1, 2, 3]) // 数组有 length,OK
// logLength(123)    // ❌ number 没有 length

// 默认类型:不传 T 时用兜底
interface ApiResp<T = unknown> {
  code: number
  data: T
}
const r1: ApiResp = { code: 0, data: null }      // data 是 unknown
const r2: ApiResp<string> = { code: 0, data: 'ok' }

条件类型:类型层面的「三元表达式」,根据类型关系选不同结果。

// 如果 T 是数组,取出元素类型;否则原样返回
type ElementOf<T> = T extends (infer E)[] ? E : T
type A = ElementOf<number[]> // number
type B = ElementOf<string>   // string

这里的 infer 表示「让 TS 自己推断这里用什么类型」。infer 是类型体操的标配,后面章节会大量用到。

名词解释

泛型约束(extends on generics):给类型参数套上「最低门槛」,写法 <T extends 某结构>。意思是「T 可以是任意类型,但必须满足这个结构」。它解决了「泛型太自由导致内部用不了某些属性」的问题——约束后你就能在泛型函数里放心访问 v.length 之类的字段。

条件类型(Conditional Type):类型世界的 条件 ? 真 : 假,写法 T extends U ? X : Y。它让类型能「根据别的类型动态变化」,是高级类型运算(类型体操)的发动机。infer 则是「在条件类型里捕获某个未知类型」的关键字。

课后练习

练习 1:写泛型 merge<T extends object>(a: T, b: T): T 合并两个对象。

答案:

function merge<T extends object>(a: T, b: T): T {
  return { ...a, ...b }
}

练习 2:type NotNull<T> = T extends null | undefined ? never : T,求 NotNull<string | null>。

答案:string。因为联合类型会「逐个分发」到条件类型上:string extends null|undefined 为假 → string;null extends ... 为真 → never;合并得 string | never = string。

总结

泛型约束和默认类型,是让泛型「既灵活又安全」的两道保险。我的看法:extends 约束不是限制自由,而是划出你能安全使用的边界——没有它,你在泛型函数体里连 v.length 都不敢用,因为编译器不知道 v 有没有。条件类型则是「类型编程」真正的开关,初学时觉得绕很正常,关键是记住它和 ?: 三元长得像、想得也一样。我建议别一上来就钻 infer 黑魔法,先把「约束 + 默认」用熟,等你需要「根据输入类型算输出类型」时,条件类型会自然成为你的工具。