請別誤用 TypeScript 過載函式型別

邊城發表於2021-11-09

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

最後總結&強調一下:過載函式型別和參與過載型別的各函式型別的聯合是完全不同的兩種型別,請注意區別,不要誤用。

相關文章