在 JavaScript 中,函數是構建應用的一塊基石,我們可以使用函數抽離可復用的邏輯、抽象模型、封裝過程。在TypeScript中,函數仍然是最基本、最重要的概念之一。下面就來看看TypeScript中的函數類型是如何定義和使用的。
一、函數類型定義
1. 直接定義
函數類型的定義包括對參數和返回值的類型定義:
- function add(arg1: number, arg2: number): number {
- return x + y;
- }
- const add = (arg1: number, arg2: number): number => {
- return x + y;
- };
這里用function字面量和箭頭函數兩種形式定義了add函數。函數參數 arg1 和 arg2 都是數值類型,最后通過相加得到的結果也是數值類型。
如果在這里省略參數的類型,TypeScript 會默認這個參數是 any 類型;如果省略返回值的類型,如果函數無返回值,那么 TypeScript 會默認函數返回值是 void 類型;如果函數有返回值,那么 TypeScript 會根據定義的邏輯推斷出返回類型。
需要注意,在TypeScript中,如果函數沒有返回值,并且我們顯式的定義了這個函數的返回值類型為 undefined,那就會報錯:A function whose declared type is neither 'void' nor 'any' must return a value。正確的做法就是上面說的,將函數的返回值類型聲明為void:
- function fn(x: number): void {
- console.log(x)
- }
一個函數的定義包括函數名、參數、邏輯和返回值。為函數定義類型時,完整的定義應該包括參數類型和返回值類型。上面都是在定義函數的指定參數類型和返回值類型。下面來定義一個完整的函數類型,以及用這個函數類型來規定一個函數定義時參數和返回值需要符合的類型。
- let add: (x: number, y: number) => number;
- add = (arg1: number, arg2: number): number => arg1 + arg2;
- add = (arg1: string, arg2: string): string => arg1 + arg2; // error
這里定義了一個變量 add,給它指定了函數類型,也就是(x: number, y: number) => number,這個函數類型包含參數和返回值的類型。然后給 add 賦了一個實際的函數,這個函數參數類型和返回類型都和函數類型中定義的一致,所以可以賦值。后面又給它賦了一個新函數,而這個函數的參數類型和返回值類型都是 string 類型,這時就會報如下錯誤:
- 不能將類型"(arg1: string, arg2: string) => string"分配給類型"(x: number, y: number) => number"。
- 參數"arg1"和"x" 的類型不兼容。
- 不能將類型"number"分配給類型"string"。
注意: 函數中如果使用了函數體之外定義的變量,這個變量的類型是不體現在函數類型定義的。
2. 接口定義
使用接口可以清晰地定義函數類型。下面來使用接口為add函數定義函數類型:
- interface Add {
- (x: number, y: number): number;
- }
- let add: Add = (arg1: string, arg2: string): string => arg1 + arg2;
- // error 不能將類型“(arg1: string, arg2: string) => string”分配給類型“Add”
通過接口形式定義了函數類型,這個接口Add定義了這個結構是一個函數,兩個參數類型都是number類型,返回值也是number類型。當指定變量add類型為Add時,再要給add賦值,就必須是一個函數,且參數類型和返回值類型都要滿足接口Add,顯然這個函數并不滿足條件,所以報錯了。
3. 類型別名定義
可以使用類型別名來定義函數類型,這種形式更加直觀易讀:
- type Add = (x: number, y: number) => number;
- let add: Add = (arg1: string, arg2: string): string => arg1 + arg2;
- // error 不能將類型“(arg1: string, arg2: string) => string”分配給類型“Add”
使用type關鍵字可以給任何定義的類型起一個別名。上面定義了 Add 這個別名后,Add就成為了一個和(x: number, y: number) => number一致的類型定義。上面定義了Add類型,指定add類型為Add,但是給add賦的值并不滿足Add類型要求,所以報錯了。
注意,這里的=>與 ES6 中箭頭函數的=>不同。TypeScript 函數類型中的=>用來表示函數的定義,其左側是函數的參數類型,右側是函數的返回值類型;而 ES6 中的=>是函數的實現。
二、函數參數定義
1. 可選參數
TypeScript 會在編寫代碼時就檢查出調用函數時參數中存在的一些錯誤:
- type Add = (x: number, y: number) => number;
- let add: Add = (arg1, arg2) => arg1 + arg2;
- add(1, 2); // success
- add(1, 2, 3); // error 應有 2 個參數,但獲得 3 個
- add(1); // error 應有 2 個參數,但獲得 1 個
在JavaScript中,上面代碼中后面兩個函數調用都不會報錯, 只不過add(1, 2, 3)可以返回正確結果3,add(1)會返回NaN。而在TypeScript中我們設置了指定的參數,那么在使用該類型時,傳入的參數必須與定義的參數類型和數量一致。
但有時候,函數有些參數不是必須的,我們就可以將函數的參數設置為可選參數。可選參數只需在參數名后跟隨一個?即可:
- type Add = (x: number, y: number, z?: number) => number;
- let add: Add = (arg1, arg2, arg3) => arg1 + arg2 + arg3;
- add(1, 2); // success 3
- add(1, 2, 3); // success 6
上面的代碼中,z是一個可選參數,那他的類型就是number | undefined,表示參數 z 就是可缺省的,那是不是意味著可缺省和類型是 undefined 等價呢?來看下面的例子:
- function log(x?: number) {
- console.log(x);
- }
- function log1(x: number | undefined) {
- console.log(x);
- }
- log();
- log(undefined);
- log1(); // Expected 1 arguments, but got 0
- log1(undefined);
可以看到,第三次函數調用報錯了,這里的 ?: 表示在調用函數時可以不顯式的傳入參數。但是,如果聲明了參數類型為 number | undefined,就表示函數參數是不可缺省且類型必須是 number 或者 undfined。
需要注意,可選參數必須放在必選參數后面,這和在 JS 中定義函數是一致的。來看例子:
- type Add = (x?: number, y: number) => number; // error 必選參數不能位于可選參數后。
在TypeScript中,可選參數必須放到最后,上面把可選參數x放到了必選參數y前面,所以報錯了。在 JavaScript 中是沒有可選參數這個概念的,只不過在編寫邏輯時,可能會判斷某個參數是否為undefined,如果是則說明調用該函數的時候沒有傳這個參數,要做下兼容處理;而如果幾個參數中,前面的參數是可不傳的,后面的參數是需要傳的,就需要在該可不傳的參數位置傳入一個 undefined 占位才行。
2. 默認參數
在 ES6 標準出來之前,默認參數實現起來比較繁瑣:
- var count = 0;
- function counter(step) {
- step = step || 1;
- count += step;
- }
上面定義了一個計數器增值函數,這個函數有一個參數 step,即每次增加的步長,如果不傳入參數,那么 step 接受到的就是 undefined,undefined 轉換為布爾值是 false,所以 step || 1 這里取了 1,從而達到了不傳參數默認 step === 1 的效果。
在 ES6 中,定義函數時給參數設默認值直接在參數后面使用等號連接默認值即可:
- const count = 0;
- const counter = (step = 1) => {
- count += step;
- };
當為參數指定了默認參數時,TypeScript 會識別默認參數的類型;當調用函數時,如果給這個帶默認值的參數傳了別的類型的參數則會報錯:
- const add = (x: number, y = 2) => {
- return x + y;
- };
- add(1, "ts"); // error 類型"string"的參數不能賦給類型"number"的參數
當然也可以顯式地給默認參數 y 設置類型:
- const add = (x: number, y: number = 2) => {
- return x + y;
- };
注意:函數的默認參數類型必須是參數類型的子類型,如下代碼:
- const add = (x: number, y: number | string = 2) => {
- return x + y;
- };
這里 add 函數參數 y 的類型為可選的聯合類型 number | string,但是因為默認參數數字類型是聯合類型 number | string 的子類型,所以 TypeScript 也會檢查通過。
3. 剩余參數
在 JavaScript 中,如果定義一個函數,這個函數可以輸入任意個數的參數,那么就無法在定義參數列表的時候挨個定義。在 ES6 發布之前,需要用 arguments 來獲取參數列表。arguments 是一個類數組對象,它包含在函數調用時傳入函數的所有實際參數,它還包含一個 length 屬性,表示參數個數。下面來模擬實現函數的重載:
- function handleData() {
- if (arguments.length === 1) return arguments[0] * 2;
- else if (arguments.length === 2) return arguments[0] * arguments[1];
- else return Array.prototype.slice.apply(arguments).join("_");
- }
- handleData(2); // 4
- handleData(2, 3); // 6
- handleData(1, 2, 3, 4, 5); // '1_2_3_4_5'
這段代碼如果在TypeScript環境中執行,三次handleData的調用都會報錯,因為handleData函數定義的時候沒有參數。
在 ES6 中,加入了…拓展運算符,它可以將一個函數或對象進行拆解。它還支持用在函數的參數列表中,用來處理任意數量的參數:
- const handleData = (arg1, ...args) => {
- console.log(args);
- };
- handleData(1, 2, 3, 4, 5); // [ 2, 3, 4, 5 ]
在 TypeScript 中可以為剩余參數指定類型:
- const handleData = (arg1: number, ...args: number[]) => {
- };
- handleData(1, "a"); // error 類型"string"的參數不能賦給類型"number"的參數
三、函數重載
在多數的函數中,是只能接受一組固定的參數。但是一些函數可以接收可變數量的參數、不同類型的參數,甚至可以根據調用函數的方式來返回不同類型的參數。要使用此類函數,TypeScript我們提供了函數重載功能。下面來看看函數重載是如何工作的。
1. 函數簽名
先來看一個簡單的例子:
- function greet(person: string): string {
- return `Hello, ${person}!`;
- }
- greet('World'); // 'Hello, World!'
這里的greet方法接收一個參數name,類型為string。那如果想讓greet方法來接收一組名稱怎么辦?那這時greet函數會接收字符串或字符串數組作為參數,并返回字符串或字符串數組。那該如何改造這個函數?主要有兩種方式:通過判斷參數類型來修改函數簽名
- function greet(person: string | string[]): string | string[] {
- if (typeof person === 'string') {
- return `Hello, ${person}!`;
- } else if (Array.isArray(person)) {
- return person.map(name => `Hello, ${name}!`);
- }
- throw new Error('error');
- }
- greet('World'); // 'Hello, World!'
- greet(['TS', 'JS']); // ['Hello, TS!', 'Hello, JS!']
這是最簡單直接的方式,但是在某些情況下,我們希望單獨定義調用函數的方式,這時就可以使用函數重載。
2. 函數重載
當修改函數簽名的方式比較復雜或者涉及到多種數據類型時,建議使用函數重載來完成。在函數重載中,我們需要定義重載簽名和實現簽名。重載函數簽名只定義函數的參數和返回值類型,并不會定義函數的正文。對于一個函數不同的調用方式,就可以有多個重載簽名。
下面來實現greet()函數的重載:
- // 重載簽名
- function greet(person: string): string;
- function greet(persons: string[]): string[];
- // 實現簽名
- function greet(person: unknown): unknown {
- if (typeof person === 'string') {
- return `Hello, ${person}!`;
- } else if (Array.isArray(person)) {
- return person.map(name => `Hello, ${name}!`);
- }
- throw new Error('error');
- }
這里greet()函數有兩個重載簽名和一個實現簽名。每個重載簽名都描述了調用函數的一種方式。我們可以使用字符串參數或使用字符串參數數組來調用greet()函數。
現在就可以使用字符串或字符串數組的參數調用greet()):
- greet('World'); // 'Hello, World!'
- greet(['TS', 'JS']); // ['Hello, TS!', 'Hello, JS!']
在定義函數重載時,需要注意以下兩點:
(1)函數簽名是可以調用的
雖然上面我們定義了重載簽名和簽名方法,但是簽名實現時不能直接調用的,只有重載簽名可以調用:
- const someValue: unknown = 'Unknown';
- greet(someValue);
這樣調用的話就會報錯:
- No overload matches this call.
- Overload 1 of 2, '(person: string): string', gave the following error.
- Argument of type 'unknown' is not assignable to parameter of type 'string'.
- Overload 2 of 2, '(persons: string[]): string[]', gave the following error.
- Argument of type 'unknown' is not assignable to parameter of type 'string[]'
也就是說,即使簽名實現接收unknown類型的參數,但是我們不能直接給greet()方法來傳遞unknown類型的參數,參數只能是函數重載簽名中定義的參數類型。
(2)實現簽名必須是通用的
在實現簽名時,定義的數據類型需要是通用的,以包含重載簽名。假如我們把greet()方法的返回值類型定義為string,這時就會出問題了:
- function greet(person: string): string;
- function greet(persons: string[]): string[];
- function greet(person: unknown): string {
- // ...
- throw new Error('error');
- }
此時string[]類型就會和string不兼容。所以,實現簽名的返回類型和參數類型都要包含所有重載簽名中的參數類型和返回值類型,保證是通用的。
3. 方法重載
除了常規函數外,類中的方法也可以過載,比如用重載方法greet()來實現一個類:
- class Greeter {
- message: string;
- constructor(message: string) {
- this.message = message;
- }
- greet(person: string): string;
- greet(persons: string[]): string[];
- greet(person: unknown): unknown {
- if (typeof person === 'string') {
- return `${this.message}, ${person}!`;
- } else if (Array.isArray(person)) {
- return person.map(name => `${this.message}, ${name}!`);
- }
- throw new Error('error');
- }
- }
Greeter類中包含了greet()重載方法:這里面有兩個描述如何調用方法的重載簽名,以及包含其實現簽名。這樣我們就可以通過兩種方式調用hi.greet():
- const hi = new Greeter('Hi');
- hi.greet('World'); // 'Hello, World!'
- hi.greet(['TS', 'JS']); // ['Hello, TS!', 'Hello, JS!']
原文鏈接:https://mp.weixin.qq.com/s/dnPhSjRQIJrukBuC58S9CA