优雅永不过时!你试过这样的函数重载吗?

文章 1小时前 juejinhot
0 0

之前和大家提过,想要写一个多参重载场景中能够将函数实现分离的通用方法。经过一些天在类型体操中的折腾,今天,它来了!

如果你也遇到类似问题或场景,开发和维护陷入混乱和挣扎的话,不妨一起来看看。

前言

在编写一些通用库/通用方法时,为了支持多种使用方式,就需要有多个参数,参数的数量和类型不同,所对应的处理方法也不同。

假如现在有这样的需求:

是不是已经开始头皮发麻了呢,不过别慌,我们一步步来看。很明显这是一个函数重载的场景,TS 原生支持函数重载,于是我们会这样写:

吼!这个参数的类型,要晕了,于是甚至可能这样写:

直接摆烂,参数交给 any[] ,变成 anyscript。几乎放弃了函数实现里的类型推导和检查。这还只是函数签名而已,函数体怎么写,可能会是这样:

一眼望不到头的 if else,这可怎么办?有一种经典的解决方案叫参数归一化

但其实也只是把 if else 换了个位置,虽然相比之下简洁方便了很多,但还是不够优雅。

这个时候突然想要推出一个新功能,还要添加新的函数签名,吼🙃!又要埋进写好的代码中摸索。😱

TS 函数重载不够强大的根本原因在于它只存在于编译时,要实现真正的重载肯定需要嵌入运行时代码。如果能够以极少量代码实现运行时的函数匹配,并在开发过程中保留完整的类型检查和提示,将函数实现真正分离,那开发体验肯定会大大提高。

想一想,如果我们能这样写代码:

整洁的观感,一目了然的函数签名定义,各个函数实现完全分离,不再是满屏的 if else,并且保持完善的 TS 类型检查和代码提示。将来如果增加一些新的函数签名,也不需要修改已有代码,遵循开闭原则。

是不是感觉优雅了很多呢!话不多说,一起来看看吧。

听了没感觉?要试过才知道。在线演练场,来试一试吧

优雅永不过时!!!

安装

npm install overload-func

使用

  1. 定义重载

调用 createOverloadedFunction 方法,需要一个类型参数,传入一个数组,每一项都是一个函数类型。

import { createOverloadedFunction } from 'overload-func';

const func = createOverloadedFunction<[
  (a: string) => string,
  (a: number, b: number) => boolean
]>();
  1. 添加实现

调用 addImple 方法,最后一个参数为函数实现,之前的各个参数都是字符串,对应不同的参数类型。

func.addImple('string', (a) => {
  return a;
});
func.addImple('number', 'number', (a, b) => {
  return a > b;
});

TS 会根据传入的参数类型,自动推导匹配对应的函数类型。

如果匹配不到相应的函数类型,或者定义的实现函数参数或返回值类型不匹配,TS 就会报错,拥有完善的类型检查和提示。

小技巧:调用 addImple 方法时,先写好最后一个函数参数占位,再写前面的参数类型,就可以随时获得代码补全提示,

更多内置类型详见 内置类型

  1. 调用

和 TS 原生的函数重载一样,调用时只需要传入正确的参数类型即可。

const r1 = func('hello'); // string
const r2 = func(1, 2); // boolean

会自动匹配到对应的函数实现,并返回结果,并且 TS 也会提示出正确的返回类型。

在线演练场,上手试一试吧

使用细节

内置类型

内置的类型支持:(字符串 -- 对应类型)

  • string -- string
  • number -- number
  • boolean -- boolean
  • null -- null
  • undefined -- undefined
  • symbol -- symbol
  • bigint -- bigint
  • function -- Function
  • array -- any[]
  • date -- Date
  • map -- Map
  • set -- Set
  • weakmap -- WeakMap
  • weakset -- WeakSet
  • regexp -- RegExp
  • promise -- Promise
  • error -- Error
  • object -- object

目前支持这些类型,包含所有基本类型,以及一些常用的内置类型。能够满足大部分的场景。

需要注意的是,object 类型不能和其他内置类型匹配,例如 any[]Map 等,这些类型本该满足 extends object 的条件,但是为了更好的作区分,内部判断时不为 object 类型的其他内置类型,是不会被认为匹配 object 类型的。例如这样:

const fun = createOverloadedFunction<[
  (a: string[]) => string
]>();
fun.addImple('object', (a) => a.join('')); // error
fun.addImple('array', (a) => a.join(''));

你不能拿 object 参数去匹配 string[],虽然这在 TS 中看起来是正常的,但在这里你需要用 array 来匹配数组类型。

源码中使用了一个 LooseEqual 类型工具来匹配函数参数类型

export type LooseEqual<X, Y> = Equal<Y, object> extends true
  ? X extends BaseType
    ? false
    : X extends Y
    	? true
    	: false
  : X extends Y ? true : false;

其中 BaseTypeobject 以外的其他内置类型object 类型会单独处理,不会和其他内置类型匹配。

可选参数

目前不支持在函数签名中使用可选参数

例如:(a: number, b?: string) => boolean,如果使用这样的可选参数,使用中是可能会出错的。因为类似于 func(1) 这样的调用,没法正确匹配到函数实现。暂时还没有想到好的解决方案。

我们可以通过下面的方式处理需要可选参数的场景。

const fn = createOverloadedFunction<[
  (a: number) => boolean,
  (a: number, b: string) => boolean,
]>();

不过话说回来,可选参数的场景,在函数实现中就存在判断参数类型的逻辑。这好像和我们使用这个库编写重载代码的初衷相悖吧😂。当然大家有什么好的想法,欢迎交流指教。

结构化类型

TS 是结构化类型系统,所以我们在推导类型、定义使用重载、处理使用中遇到的问题时,一定要从结构化类型的角度出发来考虑问题。

看下面的例子(使用了后面会介绍到的 拓展类型)

class Person {
  constructor(public name: string, public age: number) {}
}
const extendType = createExtendType({
  person: Person,
});
const fn = createOverloadedFunction<[
  (a: { name: string, age: number }) => number,
  (a: Person) => boolean
], typeof extendType>({
  extendType: extendType
});
fn.addImple('object', (a) => a.age);
fn.addImple('person', (a) => a.age > 18); // error

在上面的例子中,两个实现匹配到的都是第一个函数签名(运行起来虽然会得到想要的结果,但是 TS 会报错)。因为 TS 是结构化类型,Person 类型和 { name: string, age: number } 是兼容的。

如果确实需要上面的功能,就需要两个对象拥有明确区别的属性。我们可以为 Person 添加一个 gender 属性,为 { name: string, age: number } 添加一个 id 属性。这样一来,就能正确匹配到各自的函数签名。

高阶指引

createOverloadedFunction 方法支持一些配置选项,可以更灵活地定制函数重载。

为一个重载添加多个实现

默认情况下,一个重载只允许添加一个实现。如果需要允许多个实现,可以设置 allowMultiple 配置选项,设置为 true 时,可以为一个重载添加多个实现。

const func = createOverloadedFunction<[
  (a: string) => string,
  (a: number, b: number) => boolean
]>({ allowMultiple: true });

func.addImple('string', (a) => {
  console.log('first implementation');
  return a;
});
func.addImple('string', (a) => {
  console.log('second implementation');
  return a.toUpperCase();
});

const r1 = func('hello'); // HELLO

此时,调用函数并传入一个 string 类型参数,会依次调用两个实现函数。但是要注意,返回值为最后一个实现函数的返回值

拓展类型

extendType 参数允许扩展类型支持,可以为 addImple 方法拓展可选类型参数。

通过创建类来定义类型,传入对象,键名将作为 addImple 方法的可选类型参数,类作为键值。这里推荐使用 createExtendType 方法创建拓展类型(可以得到更好的类型检查)。

  1. 把函数的返回值传入 extendType 参数
  2. 把函数返回值的类型传入 createOverloadedFunction 的第二个类型参数
class Teacher {
  salary: number;
  constructor(public name: string) {}
}
class Student {
  score: number;
  constructor(public name: string) {}
}
const extendType = createExtendType({
  teacher: Teacher,
  student: Student,
});
const test = createOverloadedFunction<[
  (t: Teacher) => string,
  (s: Student) => number,
], typeof extendType>({
  extendType: extendType
});
test.addImple('teacher', (t) => t.name);
test.addImple('student', (s) => s.name.length);
const res1 = test(new Teacher('John'));
const res2 = test(new Student('Alice'));
console.log(res1, res2); // John 5

正如之前 结构化类型 中提到的问题,TS 是结构化类型系统。所以上面的例子中,为了区分 TeacherStudent,它们必须拥有能够区分彼此的不同属性。

当通过 extendType 拓展类型时,addImple 方法的可选类型参数就会增加 teacherstudent,同时也会有相应的代码提示。

在类中使用

要在类中使用函数,重要的一点就是正确处理 this 的指向,并且在 TS 类型中正确推导它。如果你 TS 写的还不错,那这和上面例子的使用并没有大的区别。

const test = createOverloadedFunction<[
  (this: Test, n: number) => boolean,
  (this: Test, n: string, s: string) => string,
]>();

test.addImple('number', function(n) {
  return n > this.count;
});
test.addImple('string', 'string', function(n, m) {
  return n + m;
});
class Test {
  count = 10
  test = test
}
const t = new Test();

console.log(t.test(8));
console.log(t.test('pknk', 'lll'));
  1. 在定义函数签名时,需要使用 this 类型来指定 this 的指向。
  2. 在添加实现函数时,不能使用箭头函数,而是使用普通函数。这是 JS 基础知识,这里就不做赘述。

这样就可以实现重载的同时,拥有正确的 this 类型推导。

写在最后

灵感源自于 渡一教育-袁老师 的一期短视频(感兴趣的朋友抖音搜索观看),在其基础上添加了完善的 TS 支持,并且改进了部分实现,拓展了更多功能。

其实这个库打包后的运行时 JS 代码一共也没有多少,重点在于完善的 TS 支持,提升开发体验和可维护性。

当然可能还有问题、bug、或是其他我没有考虑到的地方,欢迎交流呀。👉🏻评论区 ✈️ issue

附上源码地址: github 地址    gitee 地址

感觉怎么样,来试一试吧

版权声明:juejinhot 发表于 2025-09-28 12:15:42。
转载请注明:优雅永不过时!你试过这样的函数重载吗? | 程序员导航网

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...