谈谈什么是 Typescript 的重载

655 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

什么是重载

首先用简单的一句话定义一下什么是重载: 重载决策是一种编译时机制,用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用

重载这个概念是在一些强类型语言中才有的,在 JS 中根据函数的输入不同执行不同的逻辑是很常见的,并且依托于 TypeScript 也能够实现函数的重载,不同的入参会有着同一个实现签名,但是其实 TypeScript 的重载是伪重载,为什么这么说,本片文章的后面再来解释。

TypeScript 中的 重载

对于 TypeScript 的重载来说,它分为两个部分,一个是重载签名,一个是实现签名。

image.png

重载签名

重载签名定义了函数中每个参数的类型以及函数的返回值的类型,但是不会包含函数本体,一个函数可以有多个重载签名

实现签名

实现签名定义的参数和返回值类型必须将重载签名中所有的类型都包含在内,并且还会有着实现的函数体,一个函数只能拥有一个实现签名

真实例子

来看一段 TypeScript 的重载代码

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: string | number, b: string | number) {
    if (typeof a === 'string' || typeof b === 'string') {
        return a.toString() + b.toString();
    }
    return a + b;
}

console.log(add(1,2)) // 3

对于上面这个add函数来说,前面四个就是重载签名,最后一个是实现签名,重载签名定义了add的四种不同的入参出参,然后再实现签名中的类型必须包含重载签名中的类型,并且实现签名就是真正的函数体。

在我看来 TypeScript 的重载就只是给我们提供了一个方法,去定义同一个函数的不同的输入输出,实际上的实现还是需要再函数体中通过js逻辑去实现。

TypeScript 中的 重载有什么用?

就像上面说的,无论定义了多少个重载签名,最后还是需要在实现签名中去做一个判断,那么 TypeScript 的 重载 到底有什么用呢?

来及一个例子,我们现在定义一个数组删除方法,它可以根据索引或者是元素来指定删除数组的某一项。

如果用js我们需要怎么去写呢,是不是这个函数需要输入一个需要删减的数组本身,还有一个参数代表是通过索引删除还是元素删除。

function arrDelete(arr,position){
    let newArr = []
    if(typeof position == 'number'){
        // 执行通过索引删除的逻辑 并且赋值给新数组
        return newArr
    }
    // 执行元素删除逻辑 并且赋值给新数组
    return newArr
}

很明显,上面这段代码很容易发生运行时错误,因为一旦你的入参没有按照函数给的入参来的话,比方说第一个arr参数传入了一个 布尔 值,那么后面对它进行数组操作肯定会报错,这就是运行时错误。

那么 TypeScript 就是希望防止运行时错误,让我们能写出绝对安全的代码,那么如果我们要用 TypeScript 来改写一下上面这个函数,对于第二个输入,它有两种可能情况,数字 number 或者 一个函数 function,这个函数是用来匹配元素使用的。那么第一想到的可能会是联合类型,这是一种解决方法:

function arrDelete<T>(arr: T[],position: (item:T) => boolean | number): T[]{
    let newArr = []
    if(typeof position == 'number'){
        // 执行通过索引删除的逻辑 并且赋值给新数组
        return newArr
    }
    // 执行元素删除逻辑 并且赋值给新数组
    return newArr
}

但是这种方法的缺点也很明显,入参的定义过于广泛,万一有着更多种可能,那么会照成使用者的学习成本增加,所以在这种情况下就出现了 TypeScript 重载:

export function deleteFromArray<T>(array: T[], position: number ): T[];
export function deleteFromArray<T>(array: T[], predicate: (item: T) => boolean): T[];
export function deleteFromArray<T>(array: T[], position: number | ((item) => boolean) ): T[] {
    let newArr = []
    if(typeof position == 'number'){
        // 执行通过索引删除的逻辑 并且赋值给新数组
        return newArr
    }
    // 执行元素删除逻辑 并且赋值给新数组
    return newArr
}

这样是不是马上使得代码变得清晰了很多,我们只要通过重载签名,就能够马上看出什么样的入参会返回什么样的输出结果,再对每个不同的重载签名加上注释

/**
 * 从数组中删除若干项目。
 *
 * @export
 * @template T
 * @param {T[]} array 旧数组
 * @param {number} position 从哪个位置开始删除
 * @returns {T[]} 返回删除后的新数组
 */
export function deleteFromArray<T>(array: T[], position: number ): T[];
/**
 * 从数组中删除若干项目。
 *
 * @export
 * @template T
 * @param {T[]} array 旧数组
 * @param {(item) => boolean} predicate 用于匹配需要被删除的项目
 * @returns {T[]} 返回删除后的新数组
 */
export function deleteFromArray<T>(array: T[], predicate: (item: T) => boolean): T[];
export function deleteFromArray<T>(array: T[], position: number | ((item) => boolean) ): T[] {
    let newArr = []
    if(typeof position == 'number'){
        // 执行通过索引删除的逻辑 并且赋值给新数组
        return newArr
    }
    // 执行元素删除逻辑 并且赋值给新数组
    return newArr
}

这样是不是就能够使使用的人一眼就看出来出入参和对应的出参,也更加有利于 TypeScript 进行类型校验,这样再使用这个函数的地方,就能够最大程度的确保类型安全,使我们写出更类型更安全的代码来。

TypeScript 伪重载

写过其他强类型语言,比如java的同学,应该就能够知道,重载对于不同的输入参数是能够有不同的执行逻辑的,也就是不必像 TypeScript 一样只有一个实现签名。

对于 TypeScript 的伪重载,在上面的例子中应该也能够看得出来,虽然说我们定义了很多的 重载签名,但是最后逃不掉需要在 实现签名的函数体中去写判断来判断入参的类型,这也就是为什么说 TypeScript 的重载是一个伪重载,它更像是一种声明,只是能够更加方便使用者去理解这个函数,而没有办法方便编写者去便捷的编写函数,反而可能还会更加麻烦,这是这些麻烦,都是为了以后的更好维护。

总结

本文简单介绍了 TypeScript 的重载,以上大部分就是作者理解中的 TypeScript 重载的作用,如果有什么理解不到位或者理解错误,欢迎指出错误。