TypeScript 泛型约束与条件泛型的实际决策:什么时候用 extends、重载还是条件类型

HTMLPAGE 团队
18 分钟阅读

泛型约束、函数重载和条件类型都能处理

#TypeScript #泛型约束 #函数重载 #条件类型 #类型推导

TypeScript 有很多方式来处理"相同的函数在不同输入类型下表现不同"的场景。泛型约束(extends)限制类型范围,函数重载(overload)定义多个签名,条件类型(conditional types)在类型层面做分支。很多人会用其中一两种,但很少想清楚什么时候该用哪一种。

这三种机制不是互斥的——它们经常混用。但选错了工具会导致类型推导不准确、可读性差或维护成本高。

三种机制的定位差异

// 泛型约束:限制输入的类型范围
function identity<T extends string | number>(x: T): T {
  return x
}

// 函数重载:同一个函数的不同签名
function handle(x: string): string
function handle(x: number): number
function handle(x: string | number): string | number {
  return x // 实现签名必须兼容所有重载
}

// 条件类型:根据输入类型决定输出类型
type Output<T> = T extends string ? 'text' : T extends number ? 'num' : 'other'

简单来说:

  • 泛型约束说"我只接受某种类型"
  • 函数重载说"你的输入和输出之间有确定的对应关系"
  • 条件类型说"输出类型由输入类型的特征决定"

场景 1:限制入参,但返回值和入参类型一致

这是泛型约束最直接的用法:

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

getLength('hello')     // ✅ —— string 有 length
getLength([1, 2, 3])   // ✅ —— array 有 length
getLength(42)          // ❌ —— number 没有 length

这里用重载也能实现同样效果,但重载需要逐个列出可接受的类型:

function getLengthOverload(arg: string): number
function getLengthOverload(arg: any[]): number
function getLengthOverload(arg: { length: number }): number {
  return arg.length
}

多了一个签名就多一行重载。如果之后要支持 ArrayBuffer 或其他有 length 的类型,泛型约束不需要改代码,重载则需要加新签名。所以当"能接受什么类型"可以由结构特征描述时,泛型约束比重载更灵活。

场景 2:输入输出之间有确定的类型映射

重载的典型场景是"传入 string 返回 string,传入 number 返回 number":

function double<T extends number | string>(x: T): T extends string ? string : number
function double(x: any): any {
  if (typeof x === 'string') return x + x
  return x * 2
}

这里用了一个条件类型的返回类型,但实现上需要写 any 转换。更常见的选择是函数重载:

function double(x: string): string
function double(x: number): number
function double(x: any): any {
  if (typeof x === 'string') return x + x
  return x * 2
}

重载在这里的优势是:编译器知道 double('a') 返回 stringdouble(2) 返回 number。而且条件类型做返回类型时,在实现里经常需要类型断言。

什么时候用重载而不是条件类型:

  • 重载数量有限(一般 2-4 个)
  • 每个重载的输入输出关系是确定的、可枚举的
  • 重载签名之间的互斥性不需要额外逻辑保证

场景 3:输出类型由输入的结构特征决定

当输出类型不是"输入 A 对应输出 B"这种一对一的映射,而是需要根据输入的结构特征计算时,条件类型就上场了:

type Flatten<T> = T extends Array<infer U> ? U : T

type Flat1 = Flatten<string[]>    // string
type Flat2 = Flatten<number>      // number

type ElementType<T> = T extends Array<infer E> ? E : never
type E1 = ElementType<number[]>    // number
type E2 = ElementType<string>      // never

条件类型在这里做的是类型层面的"if 判断"——如果 T 是数组,取出元素类型;否则返回 T 本身。重载做不了这个,因为重载需要枚举所有输入,而条件类型只需要描述判断规则。

场景 4:泛型约束 + 条件类型的组合

实际情况里,三个工具经常一起用。来看一个"深度合并对象"的类型定义:

type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof A & keyof B
    ? A[K] extends Record<string, any>
      ? B[K] extends Record<string, any>
        ? Merge<A[K], B[K]>
        : A[K] | B[K]
      : A[K] | B[K]
    : K extends keyof A
      ? A[K]
      : K extends keyof B
        ? B[K]
        : never
}

这个类型做了:

  1. 取并集键
  2. 如果键在 A 和 B 中都存在,且值都是对象,递归合并
  3. 如果只在一方存在,取对应值类型

这里的泛型约束是隐式的——AB 被约束为 Record<string, any> 的扩展(因为映射类型只作用于对象)。

决策流程

三种机制的选择可以按这个流程判断:

函数需要处理多种输入类型?
├─ 输入类型可以按结构特征约束?
│  └─ 是 → 用泛型约束 <T extends { ... }>
│     └─ 输出和输入有关系?
│        ├─ 一对一的枚举关系(少于 5 种)→ 函数重载
│        └─ 由输入特征动态决定 → 条件类型
└─ 否 → 直接写联合类型入参,用 typeof 做运行时判断

常见错误

错误 1:用泛型约束替代参数类型

// 错误写法:泛型约束不必要
function getProp<T extends Record<string, any>>(obj: T, key: string): any {
  return obj[key]
}

// 更清晰的写法:直接用参数类型
function getProp(obj: Record<string, any>, key: string): any {
  return obj[key]
}

多余的泛型增加了类型参数数量,但没有增加类型安全。只有当返回值类型和输入类型有明确关系时,泛型才有意义。

错误 2:重载不足时滥用条件类型做返回类型

// 过度设计
type ReturnType<T> = T extends string ? string : number

function process<T>(x: T): ReturnType<T>
function process(x: any): any {
  return typeof x === 'string' ? x : 42
}

// 更直接
function process(x: string): string
function process(x: number): number
function process(x: any): any {
  return typeof x === 'string' ? x : 42
}

条件类型做返回类型时,实现里仍然需要写宽类型 + 类型断言,重载反而更直接。

错误 3:泛型约束太宽或太窄

// 太宽——任何类型都符合
function bad<T>(x: T): T

// 太窄——失去了泛型的意义
function bad2<T extends 'a' | 'b' | 'c'>(x: T): T

// 合适——刚好能反映需要的结构
function good<T extends string | number>(x: T): string

泛型约束的理想粒度是"刚好排除不需要的类型,又不止包含一个具体类型"。

总结

需求选用方式理由
限制入参范围泛型约束 extends结构兼容,不需要逐个枚举
有限数量签名映射函数重载声明式,类型推导准确
类型级分支计算条件类型灵活,可组合递归
泛型+重载+条件组合使用复杂签名场景的常规做法

选工具时先问一个问题:这个类型逻辑是"枚举"还是"判断"?枚举用重载,判断用条件类型,限制用泛型约束。