TypeScript 函数重载

在前面的学习中,我们已经掌握了 TypeScript 联合类型和各类参数的用法。但是,在企业级复杂业务或者开源框架的底层源码中,我们往往会遇到一种更加 “苛刻” 的需求。

假设我们需要封装一个 “查询用户” 的核心方法。后端的接口逻辑是这样的:

  • 如果传入的是用户的 ID(数字),该方法会精确匹配,返回唯一的那个 “用户对象”。
  • 如果传入的是用户的名字(字符串),该方法会进行模糊搜索,返回一个包含多人的 “用户数组”。

如果使用联合类型来定义,代码大概会写成下面这样:

const getUser = (param: number | string): User | User[] => { ... }

这种写法的致命弱点在于:当我们在外部调用并明确传入一个数字 1001 时,TypeScript 会依然 “死板” 地认为返回值有可能是数组(User | User[])。这样会导致我们在后续调用 user.name 时疯狂报错,类型推断完全失去了应有的精确度。

为了解决这种 “不同的输入类型、必须严格对应不同的输出类型” 的问题,TypeScript 提供了一个解决方案:函数重载(Function Overloads)

TypeScript 函数重载是什么?

在 TypeScript 中,函数重载简单来说就是:对于同一个函数名,我们给它定义多个不同的 “函数签名(即定义函数)”,最后再由一个统管全局的 “实现签名(即调用函数)” 来编写具体的业务逻辑。

语法:

// 1. 重载签名 1
function 函数名(参数: 类型1): 返回值类型1;

// 2. 重载签名 2
function 函数名(参数: 类型2): 返回值类型2;

// 3. 实现签名(编写真正的逻辑)
function 函数名(参数: any): any {
    // 具体的逻辑分支判断
}

说明:

可能有小伙伴会问:“为什么这里不使用箭头函数声明的语法,而是使用传统函数声明的语法呢?”

前面我们其实一直强调 “能用 const 就不用 let”,并推荐优先使用箭头函数。但在 TypeScript 中,标准的 “函数重载” 语法是强绑定 function 关键字的。

如果想要使用箭头函数来实现重载,我们需要借助极其复杂的 “交叉类型与接口调用签名”,这样会严重破坏代码的可读性。因此,在需要使用重载的特殊场景下,业界规范普遍推荐直接使用传统的 function 关键字。

示例 1:使用函数重载

// 定义接口
interface User {
    id: number;
    name: string;
}

// 重载签名 1:传数字,严格返回单个 User
function getUser(id: number): User;

// 重载签名 2:传字符串,严格返回 User 数组
function getUser(name: string): User[];

// 实现签名:参数类型和返回值类型必须兼容上面的所有重载签名
function getUser(param: number | string): any {
    // 类型收窄
    if (typeof param === "number") {
        // 模拟根据 ID 精确查询
        return { id: param, name: "阿莫" };
    } else {
        // 模拟根据姓名模糊查询
        return [
            { id: 1001, name: param },
            { id: 1002, name: param + "(小号)" }
        ];
    }
}

// 传入数字
const user1 = getUser(1001);
console.log("精确查询:", user1.name);

// 传入字符串
const user2 = getUser("Jack");
console.log("模糊查询:", user2[1].name);

运行结果如下。

精确查询:阿莫
模糊查询:Jack(小号)

TypeScript 函数重载的底层机制

在使用 TypeScript 函数重载时,有两条绝对不可触碰的底层铁律,小伙伴们一定要牢记:

1. 自上而下的匹配规则

当 TypeScript 编译器解析我们的重载调用时,它会 “从上到下” 依次去匹配你的 “重载签名”。只要匹配到第一个符合条件的,它就会立刻停止往下找。

因此,如果重载签名之间有包含关系(比如一个是 any、另一个是 string),我们必须把最精确的类型写在最上面,把最宽泛的类型写在下面。

2. 实现签名对外是 “隐身” 的

在上面的例子中,我们写了一个 function getUser(param: number | string): any 作为最终的实现签名。很多新手会误以为在调用时,也可以利用这个签名。其实这个理解是错误的!

实现签名在外部是不可见的。它仅仅是为内部的逻辑编写服务的。外部调用时,TypeScript 永远只会校验你上面写的那几个 “重载签名”。

TypeScript 进阶:参数数量不同的重载

在刚才的例子中,我们的两个重载签名接收的都是 1 个参数。但在实际业务中,我们经常会遇到参数数量不一样的情况。

比如一个 search 函数:传 1 个参数是普通搜索,传 2 个参数则可以限制返回条数。此时,在编写实现签名时,多出来的那个参数必须使用可选参数 “?” 来标记,否则 TypeScript 会认为你的实现签名无法兼容那个参数少的重载签名。

示例 2:参数数量不同的重载

// 重载签名 1:只传 1 个参数(关键字)
function search(word: string): string[];
// 重载签名 2:传 2 个参数(关键字,限制条数)
function search(word: string, limit: number): string[];

// 错误实现签名:如果没有把 limit 设为可选,会直接报错!
// function search(word: string, limit: number): any { ... }

// 正确实现签名:使用 "?" 包容参数数量较少的重载签名
function search(word: string, limit?: number): any {
    if (limit !== undefined) {
        console.log(`搜索:${word},最多返回 ${limit} 条`);
        // 模拟返回受限制的数据
        return [`${word}结果1`, `${word}结果2`];
    } else {
        console.log(`搜索:${word},返回全部`);
        // 模拟返回全部数据
        return [`${word}结果1`, `${word}结果2`, `${word}结果3`]; 
    }
}

const res1 = search("TypeScript");        // 触发重载 1
const res2 = search("TypeScript", 10);    // 触发重载 2

牢记这个坑位:实现签名必须是所有重载签名的最大公约数(包容万象)。这在封装底层高阶工具函数时非常重要。

最后说一下,函数重载是 TypeScript 类型系统走向 “动态与智能” 的分水岭。

  • 当只有参数数量不同时,我们应该优先使用:可选参数(?)和剩余参数(...)
  • 当参数类型不同、但返回值类型相同时,我们应该优先使用:联合类型(|)
  • 只有当 “不同的输入类型、会导致截然不同的输出类型” 时,我们才会去考虑使用函数重载。
给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号