请别误用 TypeScript 重载函数类型

TypeScript 允许定义重载函数类型,采多连续多个重载声明 + 一个函数实现的方式来实现。比如

function func(n: number): void;
function func(prefix: string, n: number): void;
function func(first: string | number, n?: number): void {
    if (typeof first === "string") {
        console.log(`${first}-${n}`);
    } else {
        console.log(`number-${first + 10}`);
    }
}

示例中的 func() 函数有两个重载:

  • (number) => void
  • (string, number) => void

它的实现部分,参数和返回值声明要兼容所有重载,所以第一个参数可能是 number 或者 stringfirst: string | number;而第二个参数有可能是 number 或者没有,也就是 n?: number

重载函数的声明可以用函数的接口声明方式来定义。上面的重载函数类型可以如下定义:

interface Func {
    (n: number): void;
    (prefix: string,  n: number): void;
}

检验一下:

const fn: Func = func;

以上是“序”!


现在,忘掉 func(),我们有分别定义的两个函数 func1()func2()

function func1(n: number): void {
    console.log(`number-${n + 10}`);
}

function func2(prefix: string, n: number): void {
    console.log(`${prefix}-${n}`);
}

以及有一个 render() 函数,希望根据传入的函数来渲染输出:

function render(fn: Func): void {
    if (fn.length === 2) {
        fn("hello", 9527);
    } else {
        fn(9527);
    }
}

到现在为止,一切都还没有什么问题。紧接着就是测试 render()

render(func1);
render(func2);

问题来了,不管是 func1 还是 func2,都不能正确匹配 render() 的参数类型!!!

这里有一个不少人对重载函数类型理解的误区,认为如果函数 f 符合重载函数类型 Fn 的某个重载签名,那么它就应该可以当作这个重载类型来使用。

其实不然,如果一个函数要匹配重载函数类型,那么它一定也是一个重载函数(或者兼容所有重载类型的一个函数)。拿上面的例子来说,如果传入 func1render() 运行时的确是可以准确地进入到 else 分支,并成功调用 fn(9527);传入 func2 也能准确进入 if 分支并成功调用 fn("hello", 9527) 。但是 ——

这些事情都是在运行时,由 JavaScript 干的。而 TypeScript 的编译器,在静态分析的时候发现 render() 的参数 fn 需要能够兼容 fn(string, number) 的调用,以及 fn(number) 的调用,不管是 func1() 还是 func2() 都不具备全部条件。

所以上面示例中,传入 render() 的参数只能是重载函数 func 而不能是 func1 或者 func2

那如果想达到原始目的该怎么办呢?

假设有类型 Func1Func2 分别是 func1()func2() 的类型,这个 render() 函数应该这么声明:

function render(fn: Func1 | Func2) { ... }

不过这么一来,render() 函数原来的函数体就行不通了,因为 fn 是两种类型中的一种,但在调用时并不能确定是哪种。我们需要写一个类型断言函数来帮助 TypeScript 推断。完整示例如下:

type Func1 = (n: number) => void;
type Func2 = (prefix: string, n: number) => void;

function isFunc2(fn: Func1 | Func2): fn is Func2 {
    return fn.length === 2;
}

function render(fn: Func1 | Func2): void {
    if (isFunc2(fn)) {
        fn("hello", 9527);
    } else {
        fn(9527);
    }
}

render(func1);  // number-9527
render(func2);  // hello-9527

最后总结&强调一下:重载函数类型和参与重载类型的各函数类型的联合是完全不同的两种类型,请注意区别,不要误用。


边城客栈
全栈技术专栏,公众号「边城客栈」,[链接]

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
0 条评论
推荐阅读
2022,二着二着又混过一年
收到思否小姐姐的活动提醒,才发觉又到了年底,该写“总结”了。说起总结,总有些倦——每天工作要写日报、项目上要写周报、月底要写月报、季度还有季总结,当然还有半年总结和年终总结……一年大约是 250 个工作日、50...

边城6阅读 784评论 2

封面图
JS 函数式概念: 管道 和 组合
微信搜索 【大迁世界】, 我会第一时间和你分享前端行业趋势,学习途径等等。本文 GitHub [链接] 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前端小智8阅读 723

Vue3 + Vite2 + TypeScript + Pinia(Vuex)+JSX 搭建企业级开发脚手架【开箱即用】
随着Vue3的普及,已经有越来越多的项目开始使用Vue3。为了快速进入开发状态,在这里向大家推荐一套开箱即用的企业级开发脚手架,框架使用:Vue3 + Vite2 + TypeScript + JSX + Pinia(Vuex) + Antd。废话不多话,...

阳晨@11阅读 3.2k

封面图
快速构建页面结构的 3D Visualization
可以通过 控制台 --> 右边的三个小点 --> More Tools --> Layers 打开。即可以看到页面的一个 3D 层级关系,像是这样:

chokcoco6阅读 1.6k

2022大前端总结和2023就业分析
我在年前给掘金平台分享了《2022年热点技术盘点》的前端热点,算是系统性的梳理了一下我自己对前端一整年的总结。年后,在知乎上看到《前端的就业行情怎么样?》,下面都是各种唱衰前端的论调,什么裁员,外包化...

i5ting11阅读 437评论 2

封面图
表格集算表高性能原理——怎样实现纯前端百万行数据秒级响应
集算表 (Table Sheet)是一个具备高性能渲染、数据绑定功能、公式计算能力的数据表格,通过全新构建的关系型数据管理器结合结构化公式,在高性能表格的基础上提供排序、筛选、样式、行列冻结、自动更新、单元格...

葡萄城技术团队4阅读 15.3k

8个酷炫的GitHub技巧,让你看起来像大佬一样!
微信搜索 【大迁世界】, 我会第一时间和你分享前端行业趋势,学习途径等等。本文 GitHub [链接] 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前端小智8阅读 655

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
宣传栏