TypeScript - 函式的型別

·

4 min read

在 JavaScript 中,有兩種常見的定義函式的方式——函式宣告(Function Declaration)和函式表達式(Function Expression):

// 函式宣告式(Function Declaration)
function add(x, y) {
    return x + y;
}

// 函式表達式(Function Expression)
const add = function (x, y) {
    return x + y;
};

一個函式包含了輸入與輸出。在 TypeScript 中,函式的型別是由「參數(輸入)」與「返回值(輸出)」的型別所組成的。定義函式的型別有助於確保我們在呼叫函式時,使用正確的參數並得到正確的回傳值。

函式宣告 Function Declaration

function add(x: number, y: number): number {
    return x + y;
}

輸入多餘(或者少於)函式定義的參數數量的引數,都是不被允許的

function add(x: number, y: number): number {
    return x + y;
}

// 輸入多餘的引數,會報錯
add(1,2,3) // error TS2554: Expected 2 arguments, but got 3.

// 輸入少於數量的引數,會報錯
add(1) // error TS2554: Expected 2 arguments, but got 1.

函式表達式 Function Expression

按照上面的邏輯,我們可能會把函式表達式的定義寫成這樣:

const add = function (x: number, y: number): number {
    return x + y;
}

這樣寫雖然不會報錯,不過這麼做實際上只定義了等號= 右側的匿名函式 function (x,y) {} 的型別,等號 = 左側的 add 變數是透過賦值操作進行型別推論推導出它的型別。

如果我們想要明確地指定 add 變數的型別,應該 add 變數後方加上 : 來指定其型別為函式:

const add: (x: number, y: number) => number = function (
    x:number, 
    y: number
): number {
    return x + y;
}

在 TypeScript 的型別定義中,箭頭(=>)用於表示函式的定義:

const myFunc: (parameter1: type, parameter2: type) => returnType
  • 箭頭左邊表示函式輸入的參數型別,需要用括號 () 括起來

  • 箭頭右邊表示函式輸出的返回值型別;即使函式沒有任何返回值也不能留空,須使用 void 來標示沒有返回任何值的函式

注意:不要將 TypeScript 的 => 與 ES6 的 => 搞混了。TypeScript 的型別定義中, => 用來表函式的定義;而 ES6 中的=> 則代表箭頭函示(Arrow Function)。

用介面定義函式的型別

我們也可以使用介面(Interface)來定義函式型別:

interface MathFunction {
    (x: number, y: number): number;
}

const add: MathFunction = function(x: number, y:number): number {
    return x + y;
}

const substract: MathFunction = function(x: number, y: number): number {
    returb x - y;
}

當我們想要定義多個擁有相同型別的函式時,就很適合使用介面來定義函式的形狀。

可選參數

前面有提到:「輸入多餘(或者少於)函式定義的參數數量的引數,都是不被允許的」,那麼可選的參數要如何表示呢?

介面的可選屬性類似,我們可以使用 ? 符號來參數是可選的:

function greet(name: string, age?: number): string {
    if(age){
        return `Hello, my name is ${name} and I'm ${age} years old.`
    } else {
        return `Hello, my name is ${name}.`
    }
}

console.log(greet('Alice', 30)) // 'Hello, my name is Alice and I'm 30 years old.
console.log(greet('Bob')) // 'Hello, my name is Bob.'

可選參數必須放在必須參數後面

因為 TypeScript 是根據參數的順序來確定是否傳遞了可選參數,因此可選參數應該放在必需參數之後。

如果將可選參數放在必須參數之前,TypeScript 會報錯:

// 可選參數應放在必須參數之後
function greet(age?: number, name: string): string {
    // ...
}
// error TS1016: A required parameter cannot follow an optional parameter.

設定預設值的可選參數

一旦參數設定了預設值,就代表參數是可選的。不需要再使用 ? 標記,也不會受到「可選參數必須放在必須參數之後」的限制。關於「參數預設值」在下方會有更詳細的說明。

使用可選參數時要注意未定義值 (undefined)

因為可選參數可能未傳入值且沒有預設值,所以在函數內部使用可選參數時,要注意檢查它是否為 undefined 以免出現非預期的情況。例如:

function greet(name: string, age?: number): string {
    if(age === undefined)
}

參數預設值 Default Parameter

ES6 允許我們為函式的參數新增預設值(default parameters),當函式沒有傳遞這個參數或是傳遞的值為 undefined 時,它就會作為該參數初始化的值:

// Default function parameters
function add(a, b = 1) {
    return a + b
}

console.log(add(3)) // 4
console.log(add(3, undefined)) // 4
console.log(add(3,2)) // 5

在 TypeScript 中,當我們為參數設定了預設值,TypeScript 即會將其識別為可選參數,不需要使用 ? 標記。

此時,該可選參數就不受「可選參數必須放在必須參數之後」的限制:

// 為參數加上預設值,就不受「可選參數必應放在必須參數之後」的限制
function greet(age:number = 18, name: string) {
    // ...
}

其餘參數 Rest Parameters

ES6 的其餘參數(Rest Parameter,又被翻作「剩餘參數」)讓我們表示不確定數量的參數,並將其視為一個陣列

在 TypeScript 中我們一樣可以使用其餘參數 ...rest 來處理具有不確定數量的參數的情況。我們可以用陣列的型別來定義它:

function sum(prefix: string, ...numbers: number[]): number {
    return prefix + numbers.reduce((acc, current) => acc + current, 0)
}

console.log(sum("Total: ", 1, 2)) // Total: 3
console.log(sum("Total: ", 1, 2, 3, 4, 5)) // Total: 15

需注意,其餘參數只能是最後一個參數

函式過載(Overloads)

在 TypeScript 中,函式過載(Overloads)允許我們在同一個函式定義多個不同的函式型別。TypeScript 編譯器會根據傳入的參數數量和型別為函式選擇合適的函數型別。

當函式需要根據不同型別的輸入而有不同型別的輸出時,函式過載可以讓我們根據不同的參數組合進行型別檢查,提高程式碼的可讀性和維護性。

沒有使用函式過載的例子:

function processInput(input: number | string): number | string {
    if (typeof input === "number") {
        return input * 2;
    } else {
        return `Hello, ${input}`;
    }
}

console.log(processInput(5)); // 輸出:10
console.log(processInput("Alice")); // 輸出:Hello, Alice

在這個例子中,當傳入的 input 參數是 number 型別時, procesInput 函式會回傳 number 型別的值;當傳入的 input 參數是 string 型別時,processInput 函式會回傳 string 型別的值。

然而,這樣做的缺點是當我們只看 function processInput(input: number | string): number | string 並不能確定輸入 number 時會回傳 number 還是 string

我們無法精確地表達,當輸入為數字時輸出也為數字、輸入為字串時輸出為字串,必須把整個函式內的實作內容看完才會知道。

這時,我們可以使用函式過載定義函式多個不同型別的版本

// 版本一:處理 number 的輸入,回傳 number 
function processValue(input: number): number;
// 版本二:處理 string 的輸入,回傳 string
function processValue(input: string): string;

// 最後提供一個通用的實現,接受任何型別的輸入,回傳 `any` 型別
function processValue(input: number | string): number | string {
    if (typeof input === "number") {
        return input * 2;
    } else if (typeof input === "string") {
        return `Hello, ${input}`;
    }
}

console.log(processValue(5)); // 輸出:10
console.log(processValue("Alice")); // 輸出:Hello, Alice

這個例子中,我們使用函式過載為 processValue 函式定義了兩個不同型別的版本:

  • 第一個版本處理數字輸入,回傳數字

  • 第二個版本處理字串輸入,回傳字串

最後,提供了一個通用實現 (Fallback Implementation),它接受任何型別的輸入,並回傳型別 any

函式過載的定義順序

TypeScript 會優先從最前面的函式定義開始匹配,一但找到合適的定義就會停止往下尋找。

因此當我們有多個函式定義時,要把型別定義範圍較小/較精準的寫在前面,確保 TypeScript 選擇到正確的定義。

通用實現 Fallback Implementation

在 TypeScript 中,如果無法確定函式正確的回傳型別時,需要提供一個回退版本(fallback)來確保程式碼的合法性。

如果我們將上述例子的通用實現回傳型別設定為 string | number ,會導致潛在的錯誤:

function processValue(input: number | string): number | string {
    // ..
}
// error TS2366: Function lacks ending return statement and return type does not include 'undefined'.

在函式過載中,當輸入型別沒有能夠匹配的函式版本時,會使用一個通用的、一般性的實現。這個通用實現的回傳型別通常是一個比較寬鬆的型別,例如 any。這樣做可以確保在不確定的情況下繼續保持程式碼的合法性,但同時也需要開發者自行注意處理回傳值的型別。

雖然使用 any 作為通用實現的回傳型別可以確保程式碼的合法性,但也可能會導致一些潛在的問題:

  1. 型別不確定性:使用 any 時, TypeScript 就不再執行型別檢查,這可能導致後續的程式碼會遭遇難以發現的型別錯誤。

  2. 遺漏型別檢查:如果再使用通用實現時不小心將回傳值當作特定型別處理,但實際上這個值的型別是 any,這可能會導致程式執行時出現錯誤。

  3. 遺漏過載處理:在設計函式過載時可能遺漏了特定型別的定義與處理,導致通用實現被誤用,這也是不正確的。

  4. 型別縮小:使用 any 會導致我們失去了 TypeScript 帶來的型別檢查和型別縮小(Type Narrowing) 優勢。

Ref