# TypeScript - 函式的型別 在 JavaScript 中,有兩種常見的定義函式的方式——函式宣告(Function Declaration)和函式表達式(Function Expression): ```javascript // 函式宣告式(Function Declaration) function add(x, y) { return x + y; } // 函式表達式(Function Expression) const add = function (x, y) { return x + y; }; ``` 一個函式包含了輸入與輸出。在 TypeScript 中,函式的型別是由「參數(輸入)」與「返回值(輸出)」的型別所組成的。定義函式的型別有助於確保我們在呼叫函式時,使用正確的參數並得到正確的回傳值。 ## 函式宣告 Function Declaration ```typescript function add(x: number, y: number): number { return x + y; } ``` **輸入多餘(或者少於)函式定義的參數數量的引數,都是不被允許的**: ```typescript 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 按照上面的邏輯,我們可能會把函式表達式的定義寫成這樣: ```typescript const add = function (x: number, y: number): number { return x + y; } ``` 這樣寫雖然不會報錯,不過這麼做實際上只定義了等號`=` 右側的匿名函式 `function (x,y) {}` 的型別,等號 `=` 左側的 `add` 變數是透過賦值操作進行[型別推論](https://hackmd.io/PlENakdfQUOR-A94kw0Epg)推導出它的型別。 如果我們想要明確地指定 `add` 變數的型別,應該**在 `add` 變數後方加上 `:` 來指定其型別為函式**: ```typescript const add: (x: number, y: number) => number = function ( x:number, y: number ): number { return x + y; } ``` 在 TypeScript 的型別定義中,**箭頭(`=>`)用於表示函式的定義**: ```typescript const myFunc: (parameter1: type, parameter2: type) => returnType ``` - **箭頭左邊**表示函式輸入的**參數型別**,需要用括號 `()` 括起來 - **箭頭右邊**表示函式輸出的**返回值型別**;即使函式沒有任何返回值也不能留空,須使用 [`void`](https://hackmd.io/C5c4PdVrSGuiaHEqxhsOKw?view#Void-%E7%A9%BA%E5%80%BC) 來標示沒有返回任何值的函式 注意:不要將 TypeScript 的 `=>` 與 ES6 的 `=>` 搞混了。TypeScript 的型別定義中, `=>` 用來表函式的定義;而 ES6 中的`=>` 則代表箭頭函示(Arrow Function)。 ## 用介面定義函式的型別 我們也可以使用介面(Interface)來定義函式型別: ```typescript 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; } ``` 當我們想要定義**多個擁有相同型別的函式**時,就很適合使用介面來定義函式的形狀。 ## 可選參數 前面有提到:「輸入多餘(或者少於)函式定義的參數數量的引數,都是不被允許的」,那麼可選的參數要如何表示呢? 與[介面的可選屬性](https://hackmd.io/fBKWMIwYSby3HFOGwGNWfQ#%E5%8F%AF%E9%81%B8%E5%B1%AC%E6%80%A7-Optional-Properties)類似,我們可以**使用 `?` 符號來參數是可選的**: ```typescript 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 會報錯: ```typescript // 可選參數應放在必須參數之後 function greet(age?: number, name: string): string { // ... } // error TS1016: A required parameter cannot follow an optional parameter. ``` ### 設定預設值的可選參數 一旦參數設定了預設值,就代表參數是可選的。不需要再使用 `?` 標記,也不會受到「可選參數必須放在必須參數之後」的限制。關於[「參數預設值」](https://hackmd.io/-LhUFbxaTs2Qkiz6GNh0Ew?both#%E5%8F%83%E6%95%B8%E9%A0%90%E8%A8%AD%E5%80%BC-Default-Parameter)在下方會有更詳細的說明。 ### 使用可選參數時要注意未定義值 (undefined) 因為可選參數可能未傳入值且沒有預設值,所以在函數內部使用可選參數時,要注意**檢查它是否為 `undefined`** 以免出現非預期的情況。例如: ```typescript function greet(name: string, age?: number): string { if(age === undefined) } ``` ## 參數預設值 Default Parameter ES6 允許我們為函式的參數新增預設值([default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)),當函式沒有傳遞這個參數或是傳遞的值為 `undefined` 時,它就會作為該參數初始化的值: ```javascript // 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 即會將其識別為可選參數,不需要使用 `?` 標記。 此時,該可選參數就不受「可選參數必須放在必須參數之後」的限制: ```typescript // 為參數加上預設值,就不受「可選參數必應放在必須參數之後」的限制 function greet(age:number = 18, name: string) { // ... } ``` ## 其餘參數 Rest Parameters ES6 的其餘參數(Rest Parameter,又被翻作「剩餘參數」)讓我們表示**不確定數量的參數**,並將其**視為一個陣列**。 在 TypeScript 中我們一樣可以使用其餘參數 `...rest` 來處理具有不確定數量的參數的情況。我們可以用陣列的型別來定義它: ```typescript 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 編譯器會**根據傳入的參數數量和型別**為函式選擇合適的函數型別。 當函式需要根據不同型別的輸入而有不同型別的輸出時,函式過載可以讓我們根據不同的參數組合進行型別檢查,提高程式碼的可讀性和維護性。 沒有使用函式過載的例子: ```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` 我們無法精確地表達,當輸入為數字時輸出也為數字、輸入為字串時輸出為字串,必須把整個函式內的實作內容看完才會知道。 這時,我們可以**使用函式過載定義函式多個不同型別的版本**: ```typescript // 版本一:處理 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` ,會導致潛在的錯誤: ![](https://hackmd.io/_uploads/rkLzRUVah.png) ```typescript 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 - [TypeScript 新手指南 (gitbook.io)](https://willh.gitbook.io/typescript-tutorial/) - [從零開始學 TypeScript 計畫](http://anna-yufeng.com/)