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

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

03-05 18:21