前言

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

函数重载

函数名称相同,但输入输出类型或个数不同的子程序,简单的理解就是一个同名的函数可以执行多项任务的能力。

TypeScript的函数重载

为同一个函数提供多个函数类型定义来进行函数重载,目的是为了重载的函数在调用时会进行正确的类型检查。
js因为是动态类型,本身不需要支持重载,直接对参数进行类型判读即可;但是ts为了保证类型安全,支持了函数签名的类型重载,即多个overload signatures和一个implementation signatures

JS的函数重载

C++ Java 这些静态语言中,可以通过为一个函数定义多个不同的签名,也即不同输入和输出来达到函数的重载。但是 JavaScript 的函数是没有签名的,它的输入参数是由包含零个或者多个值的数组来表示的,并且 JS 不定义参数的类型,也不检查接收的参数的类型和个数,所以在 JavaScript 中是不可能实现真正的函数重载的。
JavaScript 中没有真正意义上的函数重载,但是js支持调用一个函数时传入不同个数的入参,在函数体内进行判断执行不同逻辑,也可以实现函数重载的功能。

  1. function add(a, b) {
  2. if (typeof a === 'string' && typeof b === 'string') {
  3. return a + ',' + b;
  4. } else if (typeof a === 'number' && typeof b === 'number') {
  5. return a + b;
  6. } else {
  7. throw new Error('type error');
  8. }
  9. }
  10. add(1, 2); // 输出: "3"
  11. add('one', 'two') // 输出: "one,two"

因为js的类型提示较弱,如果让一个函数去执行判断很多逻辑的话,不仅不利于使用,而且在内部将会有很多错综复杂的类型保护判断

TS的函数重载

使用ts就可以实现真正的函数重载,获得完美的类型提示

  1. // overload signatures 函数重载签名
  2. function add(a: string, b: string): string;
  3. function add(a: number, b: number): number;
  4. // implementation signatures 函数实现签名
  5. function add(a: string | number, b: string | number): string | number {
  6. if (typeof a === 'string' && typeof b === 'string') {
  7. return a + ',' + b;
  8. } else if (typeof a === 'number' && typeof b === 'number') {
  9. return a + b;
  10. } else {
  11. throw new Error('type error');
  12. }
  13. }

image.png
使用时,当调用函数第一个入参传入number类型,后续的类型就被识别出来了
image.png
同样的,这里使用传入string类型,也完美的被识别出来了

实现重载的几个注意点

  • 因为implementation signatures对外是不可见的,当我们实现重载时,通常需要定义两个以上的overload signatures
  • implementation signatureoverload signature必须兼容,否则会type check error,如下implementationoverload不兼容image.png
  • overload signature的类型不会合并,只能resolve到一个

    一个生产环境中的例子

    有一个客户端方法shareMp用来分享图片到客户端,其中传入的参数有
参数 说明
shareType 分享的类型,微信/朋友圈海报/呼出弹窗
imgForMp 分享到小程序卡片的图片url
imgForMoments 分享朋友圈海报的图片url
title 小程序卡片的文案
path 小程序卡片点击时跳转的路径

整个函数签名如下:

  1. interface IShareOptions {
  2. shareType: 10 | 20 | 30;
  3. title?: string;
  4. path?: string;
  5. imgForMp?: string;
  6. imgForMoments?: string;
  7. }
  8. function shareMp(options: IShareOptions): void;

但是实际使用时不是所有参数都需要传入的,针对不同场景还需要区分传入的参数是否必填

说明 shareType title path imgForMp imgForMoments
分享会话小程序卡片 10
分享朋友圈海报 20
呼出分享弹窗,等待用户选择分享类型 30

使用ts的函数重载可以实现类型检查安全的函数签名:

  1. interface IShareMpForMpCardOptions {
  2. imgForMp: string;
  3. // 分享小程序卡片时,title和path是可选配置
  4. title?: string;
  5. path?: string;
  6. }
  7. interface IShareMpForMomentsOptions {
  8. imgForMoments: string;
  9. }
  10. interface IShareMpForOpenModalOptions {
  11. imgForMp: string;
  12. imgForMoments: string;
  13. title?: string;
  14. path?: string;
  15. }
  16. type IShareMpOptions =
  17. | IShareMpForMpCardOptions
  18. | IShareMpForMomentsOptions
  19. | IShareMpForOpenModalOptions;
  20. function shareMp(options: IShareMpForMpCardOptions): void;
  21. function shareMp(options: IShareMpForMomentsOptions): void;
  22. function shareMp(options: IShareMpForOpenModalOptions): void;
  23. function shareMp(options: IShareMpOptions): void {
  24. if ('imgForMp' in options && 'imgForMoments' in options) {
  25. // 呼出弹窗分享
  26. } else if ('imgForMp' in options) {
  27. // 小程序卡片分享
  28. } else if ('imgForMoments' in options) {
  29. // 朋友圈海报分享
  30. } else {
  31. throw new Error('type error');
  32. }
  33. }

implementation signature内的类型识别

第30行的地方,options由于经过类型保护,这里必定是IShareMpForOpenModalOptions
image.png
同理,在第二个if语句里,必定是IShareMpForMpCardOptions
image.png
第三个if语句里,必定是IShareMpForMomentsOptions
image.png

overload signature内的类型识别

同时传入imgForMpimgForMoments时识别为IShareMpForOpenModalOptions
image.png
当传入imgForMoments时又设置了path,就会发现3个重载的签名都没有匹配的
image.png

根据传入参数不同返回不同返回值类型的例子

  1. interface IUser {
  2. id: number;
  3. name: string;
  4. age: number;
  5. }
  6. const users: IUser[] = [
  7. { id: 0, name: 'Jessy McCree', age: 35 },
  8. { id: 1, name: 'ReinHardt', age: 52 },
  9. { id: 2, name: 'Anna', age: 48 },
  10. { id: 3, name: 'Zarya', age: 28 },
  11. { id: 4, name: 'Hanzo', age: 30 },
  12. ];
  13. function getNames(userList: IUser[], id: number): string;
  14. function getNames(userList: IUser[], id: number[]): string[];
  15. function getNames(
  16. userList: IUser[],
  17. id: number | number[],
  18. ): string | string[] {
  19. // 使用 Array.isArray 进行类型保护判断
  20. // 也可以使用 id instanceof Array 进行判断
  21. // 也可以使用 typeof id === 'object' && 'length' in id 进行判断
  22. // 只需要能够区分 number[] 和 number 都可以进行类型保护判断
  23. if (Array.isArray(id)) {
  24. const list = userList
  25. .filter((user) => {
  26. return id.includes(user.id);
  27. })
  28. .map((user) => user.name);
  29. return list;
  30. } else {
  31. const name =
  32. userList.find((user) => {
  33. return user.id === id;
  34. })?.name ?? '';
  35. return name;
  36. }
  37. }
  38. const anna = getNames(users, 2);
  39. const offensiveHeroes = getNames(users, [0, 4]);

当传入的id是number类型时,返回的结果推断为stringimage.png
当传入的id是number[]类型时,返回的结果推断为string[]
image.png

重载的顺序

进行函数重载的时候,还有一个需要遵循的原则是:越具体的类型,应该定义在越前面。这样有利于 TypeScript 编译器推断出更准确的类型。 以标准库中数组的 reduce 方法为例:

  1. interface ReadonlyArray<T> {
  2. reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;
  3. reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
  4. reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
  5. }

它有三个签名,对应着不同的参数个数和返回值,事实上标准库中的这个定义的顺序存在 bug,开发者在搜集用例打算重写它的声明,我们看一段示例代码:

  1. const list = [1, '2', 3];
  2. const str: string = list.reduce((str, a) => `${str} ${a.toString()}`, '');
  3. // const str: string
  4. // Type 'string | number' is not assignable to type 'string'.
  5. // Type 'number' is not assignable to type 'string'

image.png
在编辑器中写这段代码的时候会报错,是因为函数错误地匹配到了第二个重载签名,我们来仔细分析以下这个过程: 首先这里传入了 initialValue 参数,跳过签名一;签名二只有一个泛型参数 T,就是数组中每一项可能的类型,list 的类型是(string | number)[],所以这里Tstring | number,我们传入的 initialValue类型是string,而string是可以赋给string | number的,到了第二个这里是匹配的,所以最后返回T类型是为string | number,跟我们想要的 string 是不兼容的。
而第三个签名是更具体的,它多了一个泛型参数U,它相当于由initialValue的类型去决定了函数的返回值的类型,假设第二个签名跟第三个签名交换位置的,我们就可以得到更严格的类型 string 而不是string | number。 不过我们可以采取另外一个方法去解决这个编译错误,就是手动指定U的类型,因为只有第三个签名有泛型参数U,从而匹配到准确的重载签名:

  1. const list = [1, '2', 3];
  2. const str: string = list.reduce<string>((str, a) => `${str} ${a.toString()}`, '');

总结

  • 函数重载让编译器根据函数的输入决定函数的输出,从而推断出更准确的类型。
  • 在定义多个重载方法时,越具体的签名应该定义在越前面。