前言
ts的函数重载在很多地方都有应用,其中最常见的地方便是React.useState,其中的入参既可以是对象或者是函数,也可以是不选择传参
这三种入参类型都能识别出number类型

函数重载
函数名称相同,但输入输出类型或个数不同的子程序,简单的理解就是一个同名的函数可以执行多项任务的能力。
TypeScript的函数重载
为同一个函数提供多个函数类型定义来进行函数重载,目的是为了重载的函数在调用时会进行正确的类型检查。
js因为是动态类型,本身不需要支持重载,直接对参数进行类型判读即可;但是ts为了保证类型安全,支持了函数签名的类型重载,即多个overload signatures和一个implementation signatures。
JS的函数重载
在 C++ Java 这些静态语言中,可以通过为一个函数定义多个不同的签名,也即不同输入和输出来达到函数的重载。但是 JavaScript 的函数是没有签名的,它的输入参数是由包含零个或者多个值的数组来表示的,并且 JS 不定义参数的类型,也不检查接收的参数的类型和个数,所以在 JavaScript 中是不可能实现真正的函数重载的。
JavaScript 中没有真正意义上的函数重载,但是js支持调用一个函数时传入不同个数的入参,在函数体内进行判断执行不同逻辑,也可以实现函数重载的功能。
function add(a, b) {if (typeof a === 'string' && typeof b === 'string') {return a + ',' + b;} else if (typeof a === 'number' && typeof b === 'number') {return a + b;} else {throw new Error('type error');}}add(1, 2); // 输出: "3"add('one', 'two') // 输出: "one,two"
因为js的类型提示较弱,如果让一个函数去执行判断很多逻辑的话,不仅不利于使用,而且在内部将会有很多错综复杂的类型保护判断
TS的函数重载
使用ts就可以实现真正的函数重载,获得完美的类型提示
// overload signatures 函数重载签名function add(a: string, b: string): string;function add(a: number, b: number): number;// implementation signatures 函数实现签名function add(a: string | number, b: string | number): string | number {if (typeof a === 'string' && typeof b === 'string') {return a + ',' + b;} else if (typeof a === 'number' && typeof b === 'number') {return a + b;} else {throw new Error('type error');}}

使用时,当调用函数第一个入参传入number类型,后续的类型就被识别出来了
同样的,这里使用传入string类型,也完美的被识别出来了
实现重载的几个注意点
- 因为
implementation signatures对外是不可见的,当我们实现重载时,通常需要定义两个以上的overload signatures implementation signature和overload signature必须兼容,否则会type check error,如下implementation和overload不兼容
overload signature的类型不会合并,只能resolve到一个一个生产环境中的例子
有一个客户端方法shareMp用来分享图片到客户端,其中传入的参数有
| 参数 | 说明 |
|---|---|
| shareType | 分享的类型,微信/朋友圈海报/呼出弹窗 |
| imgForMp | 分享到小程序卡片的图片url |
| imgForMoments | 分享朋友圈海报的图片url |
| title | 小程序卡片的文案 |
| path | 小程序卡片点击时跳转的路径 |
整个函数签名如下:
interface IShareOptions {shareType: 10 | 20 | 30;title?: string;path?: string;imgForMp?: string;imgForMoments?: string;}function shareMp(options: IShareOptions): void;
但是实际使用时不是所有参数都需要传入的,针对不同场景还需要区分传入的参数是否必填
| 说明 | shareType | title | path | imgForMp | imgForMoments |
|---|---|---|---|---|---|
| 分享会话小程序卡片 | 10 | ✅ | ✅ | ✅ | |
| 分享朋友圈海报 | 20 | ✅ | |||
| 呼出分享弹窗,等待用户选择分享类型 | 30 | ✅ | ✅ | ✅ | ✅ |
使用ts的函数重载可以实现类型检查安全的函数签名:
interface IShareMpForMpCardOptions {imgForMp: string;// 分享小程序卡片时,title和path是可选配置title?: string;path?: string;}interface IShareMpForMomentsOptions {imgForMoments: string;}interface IShareMpForOpenModalOptions {imgForMp: string;imgForMoments: string;title?: string;path?: string;}type IShareMpOptions =| IShareMpForMpCardOptions| IShareMpForMomentsOptions| IShareMpForOpenModalOptions;function shareMp(options: IShareMpForMpCardOptions): void;function shareMp(options: IShareMpForMomentsOptions): void;function shareMp(options: IShareMpForOpenModalOptions): void;function shareMp(options: IShareMpOptions): void {if ('imgForMp' in options && 'imgForMoments' in options) {// 呼出弹窗分享} else if ('imgForMp' in options) {// 小程序卡片分享} else if ('imgForMoments' in options) {// 朋友圈海报分享} else {throw new Error('type error');}}
implementation signature内的类型识别
第30行的地方,options由于经过类型保护,这里必定是IShareMpForOpenModalOptions
同理,在第二个if语句里,必定是IShareMpForMpCardOptions
第三个if语句里,必定是IShareMpForMomentsOptions
overload signature内的类型识别
同时传入imgForMp和imgForMoments时识别为IShareMpForOpenModalOptions
当传入imgForMoments时又设置了path,就会发现3个重载的签名都没有匹配的
根据传入参数不同返回不同返回值类型的例子
interface IUser {id: number;name: string;age: number;}const users: IUser[] = [{ id: 0, name: 'Jessy McCree', age: 35 },{ id: 1, name: 'ReinHardt', age: 52 },{ id: 2, name: 'Anna', age: 48 },{ id: 3, name: 'Zarya', age: 28 },{ id: 4, name: 'Hanzo', age: 30 },];function getNames(userList: IUser[], id: number): string;function getNames(userList: IUser[], id: number[]): string[];function getNames(userList: IUser[],id: number | number[],): string | string[] {// 使用 Array.isArray 进行类型保护判断// 也可以使用 id instanceof Array 进行判断// 也可以使用 typeof id === 'object' && 'length' in id 进行判断// 只需要能够区分 number[] 和 number 都可以进行类型保护判断if (Array.isArray(id)) {const list = userList.filter((user) => {return id.includes(user.id);}).map((user) => user.name);return list;} else {const name =userList.find((user) => {return user.id === id;})?.name ?? '';return name;}}const anna = getNames(users, 2);const offensiveHeroes = getNames(users, [0, 4]);
当传入的id是number类型时,返回的结果推断为string
当传入的id是number[]类型时,返回的结果推断为string[]
重载的顺序
进行函数重载的时候,还有一个需要遵循的原则是:越具体的类型,应该定义在越前面。这样有利于 TypeScript 编译器推断出更准确的类型。 以标准库中数组的 reduce 方法为例:
interface ReadonlyArray<T> {reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;}
它有三个签名,对应着不同的参数个数和返回值,事实上标准库中的这个定义的顺序存在 bug,开发者在搜集用例打算重写它的声明,我们看一段示例代码:
const list = [1, '2', 3];const str: string = list.reduce((str, a) => `${str} ${a.toString()}`, '');// const str: string// Type 'string | number' is not assignable to type 'string'.// Type 'number' is not assignable to type 'string'

在编辑器中写这段代码的时候会报错,是因为函数错误地匹配到了第二个重载签名,我们来仔细分析以下这个过程: 首先这里传入了 initialValue 参数,跳过签名一;签名二只有一个泛型参数 T,就是数组中每一项可能的类型,list 的类型是(string | number)[],所以这里T是string | number,我们传入的 initialValue类型是string,而string是可以赋给string | number的,到了第二个这里是匹配的,所以最后返回T类型是为string | number,跟我们想要的 string 是不兼容的。
而第三个签名是更具体的,它多了一个泛型参数U,它相当于由initialValue的类型去决定了函数的返回值的类型,假设第二个签名跟第三个签名交换位置的,我们就可以得到更严格的类型 string 而不是string | number。 不过我们可以采取另外一个方法去解决这个编译错误,就是手动指定U的类型,因为只有第三个签名有泛型参数U,从而匹配到准确的重载签名:
const list = [1, '2', 3];const str: string = list.reduce<string>((str, a) => `${str} ${a.toString()}`, '');
总结
- 函数重载让编译器根据函数的输入决定函数的输出,从而推断出更准确的类型。
- 在定义多个重载方法时,越具体的签名应该定义在越前面。
