TypeScript - 函式的型別
在 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
作為通用實現的回傳型別可以確保程式碼的合法性,但也可能會導致一些潛在的問題:
型別不確定性:使用
any
時, TypeScript 就不再執行型別檢查,這可能導致後續的程式碼會遭遇難以發現的型別錯誤。遺漏型別檢查:如果再使用通用實現時不小心將回傳值當作特定型別處理,但實際上這個值的型別是
any
,這可能會導致程式執行時出現錯誤。遺漏過載處理:在設計函式過載時可能遺漏了特定型別的定義與處理,導致通用實現被誤用,這也是不正確的。
型別縮小:使用
any
會導致我們失去了 TypeScript 帶來的型別檢查和型別縮小(Type Narrowing) 優勢。