Menu

TypeScript 函数重载:重载签名详解

TypeScript 函数重载让一个函数拥有多个调用签名,每个签名都有自己的返回类型。学习“重载签名加实现”的写法、编译器检查的规则、什么时候改用联合类型参数更好,以及类中的重载。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 中的函数重载,是指为一个函数编写多个调用签名,后面再跟一个唯一的实现。每个签名都可以把不同的参数类型和不同的返回类型配对,调用方拿到的是最精确的那一个。

如果没有重载,parse 每次调用都会返回 number | number[],在你自己收窄结果之前,one + 1 都会报错。

重载签名与实现

一个重载函数由两部分组成:

  1. 重载签名:没有函数体的声明,每种支持的调用形式对应一个。调用方只能使用这些签名。
  2. 实现签名:最后一个带函数体的声明。它的参数必须能接受所有重载接受的内容,它的返回类型必须涵盖所有重载的返回类型。它对外部是不可见的。

类型只存在于编译期,所以运行时只有一个 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 错误表示某个重载接受或返回了实现所不支持的东西。实现的参数必须能接受每个重载的参数,返回类型也必须和每个重载的返回类型兼容。把实现的类型放宽(通常放宽为联合类型)就能解决。

Coddy 编程语言插图

和 Coddy 一起学编程

开始