TypeScript 中的函数重载,是指为一个函数编写多个调用签名,后面再跟一个唯一的实现。每个签名都可以把不同的参数类型和不同的返回类型配对,调用方拿到的是最精确的那一个。
如果没有重载,parse 每次调用都会返回 number | number[],在你自己收窄结果之前,one + 1 都会报错。
重载签名与实现
一个重载函数由两部分组成:
- 重载签名:没有函数体的声明,每种支持的调用形式对应一个。调用方只能使用这些签名。
- 实现签名:最后一个带函数体的声明。它的参数必须能接受所有重载接受的内容,它的返回类型必须涵盖所有重载的返回类型。它对外部是不可见的。
类型只存在于编译期,所以运行时只有一个 JavaScript 函数。实现必须检查自己的参数(typeof、Array.isArray、arguments.length...)来决定做什么。编译器会检查重载和实现是否一致:
function format(value: string): string;
function format(value: number): number {
return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.
解决办法是放宽实现的类型:function format(value: string | number): string | number。
实现签名不能被调用
这是最让人意外的一条规则。一次调用必须单独匹配某一个重载签名;TypeScript 不会把它们组合起来。
编译器输出:
index.ts(12,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
实现接受 string | string[],但调用方看不到它。添加第三个接受联合类型、返回联合类型的重载,这次调用就能通过编译,并打印 [ 1, 2 ]:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
参数个数不同
重载也能描述参数个数不同的调用。下面的例子中,日期可以由时间戳构建,也可以由年、月、日构建,但不能只用两个数字:
如果用一个带两个可选参数的签名,它会接受 makeDate(2024, 3),并悄悄地构建出错误的日期。重载把这种情况变成了编译错误(TS2575)。
顺序很重要
TypeScript 从上到下依次尝试重载,选用第一个匹配的。把最具体的签名放在前面。列表靠前的宽泛重载会吞掉本该交给后面签名的调用:
function describe(value: unknown): string; // matches everything
function describe(value: string): "text"; // never chosen
function describe(value: unknown): string {
return typeof value === "string" ? "text" : "other";
}
const d = describe("hi"); // d: string, not "text"
把前两个签名交换位置,describe("hi") 的类型就是 "text"。
用重载还是联合类型参数?
当返回类型取决于参数类型时,重载多写的几行才值得。如果不是这样,一个带联合类型参数的签名更短、更易读,还能接受重载会拒绝的联合类型参数。
| 使用 | 适用场景 |
|---|---|
| 联合类型参数 | 每种输入的返回类型都相同 |
| 可选参数 | 各种调用形式的区别只在于末尾那些可以随意省略的参数 |
| 重载 | 返回类型随参数变化,或者某些参数组合必须被拒绝 |
| 泛型 | 返回类型由参数类型构建而来,例如 identity<T>(x: T): T |
结合条件类型的泛型可以把某些重载组合写成一个签名,但只有两三种情况时,重载通常更易读。
方法与构造函数的重载
在类中,方法使用同样的写法:先写重载签名,再写带函数体的方法。构造函数也可以用同样的方式重载。
接口和对象类型也可以声明重载,形式是多个调用签名,或者多个同名的方法签名。很多内置函数就是这样声明的:在编辑器里把鼠标悬停在数组的 reduce 上,会显示 “+2 overloads”。
常见问题
TypeScript 支持函数重载吗?
支持,但只在类型层面。你先写多个重载签名(没有函数体的声明),后面跟一个实现。调用方只能看到重载签名。运行时仍然只有一个 JavaScript 函数,所以实现需要自己检查参数,并处理每一种情况。
“No overload matches this call” 是什么意思?
这是 TS2769 错误:传入的参数不符合任何一个重载签名。实现签名不算在内,所以即使实现能接受,用 string | string[] 这样的联合类型参数调用也会失败。可以添加一个接受该联合类型的重载,或者用一个签名替换掉所有重载。
什么时候应该用重载,而不是联合类型?
当返回类型取决于传入的参数类型时用重载,例如传入 string 返回 number,传入 string[] 返回 number[]。如果每种输入的返回类型都一样,一个带联合类型参数的签名更简单,而且也能接受联合类型的参数。
TypeScript 中箭头函数可以重载吗?
不能用重载声明的语法,它只适用于 function 声明和方法。你可以给变量一个带多个调用签名的重载类型:type Parse = { (s: string): number; (s: string[]): number[] },但把箭头函数赋值给它通常需要类型断言,所以用 function 声明更干净。
为什么我的重载签名和实现签名不兼容?
TS2394 错误表示某个重载接受或返回了实现所不支持的东西。实现的参数必须能接受每个重载的参数,返回类型也必须和每个重载的返回类型兼容。把实现的类型放宽(通常放宽为联合类型)就能解决。