使用條件型別實現TypeScript中的函式過載

forceddd發表於2021-10-26

假設有這樣的一個函式,你會怎麼來宣告他的型別呢?

function add(a,b){
    return a+b;
}

add函式可能有兩種情況:

  1. 引數a、bnumber型別,返回值為number型別
  2. 引數a、bstring型別,返回值為string型別

使用函式過載能解決嗎?

首先,你可能會通過宣告多個函式型別,來實現對add函式的過載宣告。

function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: any, b: any) {
    return a + b;
}
add(1, 2); //function add(a: number, b: number): number
add('x', 'y'); //function add(a: string, b: string): string

這種方法顯然不錯,但是有一個小問題:當傳入的引數型別是number|string時,會出現意想不到的型別錯誤。

let a:string|number;
add(a,a);
/*
  第 1 個過載(共 2 個),“(a: string, b: string): string”,出現以下錯誤。
    型別“string | number”的引數不能賦給型別“string”的引數。
      不能將型別“number”分配給型別“string”。
  第 2 個過載(共 2 個),“(a: number, b: number): number”,出現以下錯誤。
    型別“string | number”的引數不能賦給型別“number”的引數。
      不能將型別“string”分配給型別“number”。
*/

這是因為當我們使用函式過載時,TypeScript是使用這些過載來逐個比對的,直到匹配到合適的型別過載。但是顯然,我們宣告的兩種過載中的變數型別,numberstring都與number|string不匹配,所以出現了型別錯誤。

使用泛型怎麼樣呢?

然後,你還可能會想到使用泛型來宣告型別,以便構建一種通用的模式。

function add<T extends number | string>(a: T, b: T): T;
function add(a: any, b: any) {
    return a + b;
}
const a:number = 0;
const b:string='str';
add(a, a);//function add<number>(a: number, b: number): number
add(b, b);//function add<string>(a: string, b: string): string

這個方法看起來也很好,但是同樣有一個小問題:當傳入字面量型別的引數時,該引數的型別會被認為和引數的值相同的型別。例如:

add(1,2);//function add<1 | 2>(a: 1 | 2, b: 1 | 2): 1 | 2

引數1type1,而number,就如同我們宣告瞭 type T=1|2;一樣,函式型別宣告中的泛型T既要滿足第一個引數1,又要滿足第二個引數2,所以T的型別成為了1|2;同樣的,如果傳入的變數a、b沒有顯式的型別宣告numberstring,也會出現這個問題。

條件型別可能會更好

其實,在這種函式需要過載的時候使用條件型別,可能會有更好的效果。

function add<T extends number | string>(
    a: T,
    b: T
): T extends number ? number : string;
function add(a: any, b: any) {
    return a + b;
}
add(1,2);//function add<1 | 2>(a: 1 | 2, b: 1 | 2): number
let a:number|string;
add(a, a);//function add<string | number>(a: string | number, b: string | number): string | number

這種方式是先通過泛型的方法獲取到引數的型別,在使用條件型別進行判斷,從而得出正確的型別。

這種方法相較於前兩種方法寫起來更復雜,但是型別會更加準確。在函式需要型別過載時,不妨考慮一下使用條件型別來實現。

相關文章