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') 返回 string,double(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
}
这个类型做了:
- 取并集键
- 如果键在 A 和 B 中都存在,且值都是对象,递归合并
- 如果只在一方存在,取对应值类型
这里的泛型约束是隐式的——A 和 B 被约束为 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 | 结构兼容,不需要逐个枚举 |
| 有限数量签名映射 | 函数重载 | 声明式,类型推导准确 |
| 类型级分支计算 | 条件类型 | 灵活,可组合递归 |
| 泛型+重载+条件 | 组合使用 | 复杂签名场景的常规做法 |
选工具时先问一个问题:这个类型逻辑是"枚举"还是"判断"?枚举用重载,判断用条件类型,限制用泛型约束。


