詳解 TypeScript 函數(shù)聲明和重載
在 JavaScript 中,函數(shù)是構(gòu)建應(yīng)用的一塊基石,我們可以使用函數(shù)抽離可復(fù)用的邏輯、抽象模型、封裝過(guò)程。在TypeScript中,函數(shù)仍然是最基本、最重要的概念之一。下面就來(lái)看看TypeScript中的函數(shù)類型是如何定義和使用的。
一、函數(shù)類型定義
1. 直接定義
函數(shù)類型的定義包括對(duì)參數(shù)和返回值的類型定義:
- function add(arg1: number, arg2: number): number {
- return x + y;
- }
- const add = (arg1: number, arg2: number): number => {
- return x + y;
- };
這里用function字面量和箭頭函數(shù)兩種形式定義了add函數(shù)。函數(shù)參數(shù) arg1 和 arg2 都是數(shù)值類型,最后通過(guò)相加得到的結(jié)果也是數(shù)值類型。
如果在這里省略參數(shù)的類型,TypeScript 會(huì)默認(rèn)這個(gè)參數(shù)是 any 類型;如果省略返回值的類型,如果函數(shù)無(wú)返回值,那么 TypeScript 會(huì)默認(rèn)函數(shù)返回值是 void 類型;如果函數(shù)有返回值,那么 TypeScript 會(huì)根據(jù)定義的邏輯推斷出返回類型。
需要注意,在TypeScript中,如果函數(shù)沒(méi)有返回值,并且我們顯式的定義了這個(gè)函數(shù)的返回值類型為 undefined,那就會(huì)報(bào)錯(cuò):A function whose declared type is neither 'void' nor 'any' must return a value。正確的做法就是上面說(shuō)的,將函數(shù)的返回值類型聲明為void:
- function fn(x: number): void {
- console.log(x)
- }
一個(gè)函數(shù)的定義包括函數(shù)名、參數(shù)、邏輯和返回值。為函數(shù)定義類型時(shí),完整的定義應(yīng)該包括參數(shù)類型和返回值類型。上面都是在定義函數(shù)的指定參數(shù)類型和返回值類型。下面來(lái)定義一個(gè)完整的函數(shù)類型,以及用這個(gè)函數(shù)類型來(lái)規(guī)定一個(gè)函數(shù)定義時(shí)參數(shù)和返回值需要符合的類型。
- let add: (x: number, y: number) => number;
- add = (arg1: number, arg2: number): number => arg1 + arg2;
- add = (arg1: string, arg2: string): string => arg1 + arg2; // error
這里定義了一個(gè)變量 add,給它指定了函數(shù)類型,也就是(x: number, y: number) => number,這個(gè)函數(shù)類型包含參數(shù)和返回值的類型。然后給 add 賦了一個(gè)實(shí)際的函數(shù),這個(gè)函數(shù)參數(shù)類型和返回類型都和函數(shù)類型中定義的一致,所以可以賦值。后面又給它賦了一個(gè)新函數(shù),而這個(gè)函數(shù)的參數(shù)類型和返回值類型都是 string 類型,這時(shí)就會(huì)報(bào)如下錯(cuò)誤:
- 不能將類型"(arg1: string, arg2: string) => string"分配給類型"(x: number, y: number) => number"。
- 參數(shù)"arg1"和"x" 的類型不兼容。
- 不能將類型"number"分配給類型"string"。
注意: 函數(shù)中如果使用了函數(shù)體之外定義的變量,這個(gè)變量的類型是不體現(xiàn)在函數(shù)類型定義的。
2. 接口定義
使用接口可以清晰地定義函數(shù)類型。下面來(lái)使用接口為add函數(shù)定義函數(shù)類型:
- interface Add {
- (x: number, y: number): number;
- }
- let add: Add = (arg1: string, arg2: string): string => arg1 + arg2;
- // error 不能將類型“(arg1: string, arg2: string) => string”分配給類型“Add”
通過(guò)接口形式定義了函數(shù)類型,這個(gè)接口Add定義了這個(gè)結(jié)構(gòu)是一個(gè)函數(shù),兩個(gè)參數(shù)類型都是number類型,返回值也是number類型。當(dāng)指定變量add類型為Add時(shí),再要給add賦值,就必須是一個(gè)函數(shù),且參數(shù)類型和返回值類型都要滿足接口Add,顯然這個(gè)函數(shù)并不滿足條件,所以報(bào)錯(cuò)了。
3. 類型別名定義
可以使用類型別名來(lái)定義函數(shù)類型,這種形式更加直觀易讀:
- 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關(guān)鍵字可以給任何定義的類型起一個(gè)別名。上面定義了 Add 這個(gè)別名后,Add就成為了一個(gè)和(x: number, y: number) => number一致的類型定義。上面定義了Add類型,指定add類型為Add,但是給add賦的值并不滿足Add類型要求,所以報(bào)錯(cuò)了。
注意,這里的=>與 ES6 中箭頭函數(shù)的=>不同。TypeScript 函數(shù)類型中的=>用來(lái)表示函數(shù)的定義,其左側(cè)是函數(shù)的參數(shù)類型,右側(cè)是函數(shù)的返回值類型;而 ES6 中的=>是函數(shù)的實(shí)現(xiàn)。
二、函數(shù)參數(shù)定義
1. 可選參數(shù)
TypeScript 會(huì)在編寫代碼時(shí)就檢查出調(diào)用函數(shù)時(shí)參數(shù)中存在的一些錯(cuò)誤:
- type Add = (x: number, y: number) => number;
- let add: Add = (arg1, arg2) => arg1 + arg2;
- add(1, 2); // success
- add(1, 2, 3); // error 應(yīng)有 2 個(gè)參數(shù),但獲得 3 個(gè)
- add(1); // error 應(yīng)有 2 個(gè)參數(shù),但獲得 1 個(gè)
在JavaScript中,上面代碼中后面兩個(gè)函數(shù)調(diào)用都不會(huì)報(bào)錯(cuò), 只不過(guò)add(1, 2, 3)可以返回正確結(jié)果3,add(1)會(huì)返回NaN。而在TypeScript中我們?cè)O(shè)置了指定的參數(shù),那么在使用該類型時(shí),傳入的參數(shù)必須與定義的參數(shù)類型和數(shù)量一致。
但有時(shí)候,函數(shù)有些參數(shù)不是必須的,我們就可以將函數(shù)的參數(shù)設(shè)置為可選參數(shù)。可選參數(shù)只需在參數(shù)名后跟隨一個(gè)?即可:
- 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是一個(gè)可選參數(shù),那他的類型就是number | undefined,表示參數(shù) z 就是可缺省的,那是不是意味著可缺省和類型是 undefined 等價(jià)呢?來(lái)看下面的例子:
- 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);
可以看到,第三次函數(shù)調(diào)用報(bào)錯(cuò)了,這里的 ?: 表示在調(diào)用函數(shù)時(shí)可以不顯式的傳入?yún)?shù)。但是,如果聲明了參數(shù)類型為 number | undefined,就表示函數(shù)參數(shù)是不可缺省且類型必須是 number 或者 undfined。
需要注意,可選參數(shù)必須放在必選參數(shù)后面,這和在 JS 中定義函數(shù)是一致的。來(lái)看例子:
- type Add = (x?: number, y: number) => number; // error 必選參數(shù)不能位于可選參數(shù)后。
在TypeScript中,可選參數(shù)必須放到最后,上面把可選參數(shù)x放到了必選參數(shù)y前面,所以報(bào)錯(cuò)了。在 JavaScript 中是沒(méi)有可選參數(shù)這個(gè)概念的,只不過(guò)在編寫邏輯時(shí),可能會(huì)判斷某個(gè)參數(shù)是否為undefined,如果是則說(shuō)明調(diào)用該函數(shù)的時(shí)候沒(méi)有傳這個(gè)參數(shù),要做下兼容處理;而如果幾個(gè)參數(shù)中,前面的參數(shù)是可不傳的,后面的參數(shù)是需要傳的,就需要在該可不傳的參數(shù)位置傳入一個(gè) undefined 占位才行。
2. 默認(rèn)參數(shù)
在 ES6 標(biāo)準(zhǔn)出來(lái)之前,默認(rèn)參數(shù)實(shí)現(xiàn)起來(lái)比較繁瑣:
- var count = 0;
- function counter(step) {
- step = step || 1;
- count += step;
- }
上面定義了一個(gè)計(jì)數(shù)器增值函數(shù),這個(gè)函數(shù)有一個(gè)參數(shù) step,即每次增加的步長(zhǎng),如果不傳入?yún)?shù),那么 step 接受到的就是 undefined,undefined 轉(zhuǎn)換為布爾值是 false,所以 step || 1 這里取了 1,從而達(dá)到了不傳參數(shù)默認(rèn) step === 1 的效果。
在 ES6 中,定義函數(shù)時(shí)給參數(shù)設(shè)默認(rèn)值直接在參數(shù)后面使用等號(hào)連接默認(rèn)值即可:
- const count = 0;
- const counter = (step = 1) => {
- count += step;
- };
當(dāng)為參數(shù)指定了默認(rèn)參數(shù)時(shí),TypeScript 會(huì)識(shí)別默認(rèn)參數(shù)的類型;當(dāng)調(diào)用函數(shù)時(shí),如果給這個(gè)帶默認(rèn)值的參數(shù)傳了別的類型的參數(shù)則會(huì)報(bào)錯(cuò):
- const add = (x: number, y = 2) => {
- return x + y;
- };
- add(1, "ts"); // error 類型"string"的參數(shù)不能賦給類型"number"的參數(shù)
當(dāng)然也可以顯式地給默認(rèn)參數(shù) y 設(shè)置類型:
- const add = (x: number, y: number = 2) => {
- return x + y;
- };
注意:函數(shù)的默認(rèn)參數(shù)類型必須是參數(shù)類型的子類型,如下代碼:
- const add = (x: number, y: number | string = 2) => {
- return x + y;
- };
這里 add 函數(shù)參數(shù) y 的類型為可選的聯(lián)合類型 number | string,但是因?yàn)槟J(rèn)參數(shù)數(shù)字類型是聯(lián)合類型 number | string 的子類型,所以 TypeScript 也會(huì)檢查通過(guò)。
3. 剩余參數(shù)
在 JavaScript 中,如果定義一個(gè)函數(shù),這個(gè)函數(shù)可以輸入任意個(gè)數(shù)的參數(shù),那么就無(wú)法在定義參數(shù)列表的時(shí)候挨個(gè)定義。在 ES6 發(fā)布之前,需要用 arguments 來(lái)獲取參數(shù)列表。arguments 是一個(gè)類數(shù)組對(duì)象,它包含在函數(shù)調(diào)用時(shí)傳入函數(shù)的所有實(shí)際參數(shù),它還包含一個(gè) length 屬性,表示參數(shù)個(gè)數(shù)。下面來(lái)模擬實(shí)現(xiàn)函數(shù)的重載:
- 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環(huán)境中執(zhí)行,三次handleData的調(diào)用都會(huì)報(bào)錯(cuò),因?yàn)閔andleData函數(shù)定義的時(shí)候沒(méi)有參數(shù)。
在 ES6 中,加入了…拓展運(yùn)算符,它可以將一個(gè)函數(shù)或?qū)ο筮M(jìn)行拆解。它還支持用在函數(shù)的參數(shù)列表中,用來(lái)處理任意數(shù)量的參數(shù):
- const handleData = (arg1, ...args) => {
- console.log(args);
- };
- handleData(1, 2, 3, 4, 5); // [ 2, 3, 4, 5 ]
在 TypeScript 中可以為剩余參數(shù)指定類型:
- const handleData = (arg1: number, ...args: number[]) => {
- };
- handleData(1, "a"); // error 類型"string"的參數(shù)不能賦給類型"number"的參數(shù)
三、函數(shù)重載
在多數(shù)的函數(shù)中,是只能接受一組固定的參數(shù)。但是一些函數(shù)可以接收可變數(shù)量的參數(shù)、不同類型的參數(shù),甚至可以根據(jù)調(diào)用函數(shù)的方式來(lái)返回不同類型的參數(shù)。要使用此類函數(shù),TypeScript我們提供了函數(shù)重載功能。下面來(lái)看看函數(shù)重載是如何工作的。
1. 函數(shù)簽名
先來(lái)看一個(gè)簡(jiǎn)單的例子:
- function greet(person: string): string {
- return `Hello, ${person}!`;
- }
- greet('World'); // 'Hello, World!'
這里的greet方法接收一個(gè)參數(shù)name,類型為string。那如果想讓greet方法來(lái)接收一組名稱怎么辦?那這時(shí)greet函數(shù)會(huì)接收字符串或字符串?dāng)?shù)組作為參數(shù),并返回字符串或字符串?dāng)?shù)組。那該如何改造這個(gè)函數(shù)?主要有兩種方式:通過(guò)判斷參數(shù)類型來(lái)修改函數(shù)簽名
- 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!']
這是最簡(jiǎn)單直接的方式,但是在某些情況下,我們希望單獨(dú)定義調(diào)用函數(shù)的方式,這時(shí)就可以使用函數(shù)重載。
2. 函數(shù)重載
當(dāng)修改函數(shù)簽名的方式比較復(fù)雜或者涉及到多種數(shù)據(jù)類型時(shí),建議使用函數(shù)重載來(lái)完成。在函數(shù)重載中,我們需要定義重載簽名和實(shí)現(xiàn)簽名。重載函數(shù)簽名只定義函數(shù)的參數(shù)和返回值類型,并不會(huì)定義函數(shù)的正文。對(duì)于一個(gè)函數(shù)不同的調(diào)用方式,就可以有多個(gè)重載簽名。
下面來(lái)實(shí)現(xiàn)greet()函數(shù)的重載:
- // 重載簽名
- function greet(person: string): string;
- function greet(persons: string[]): string[];
- // 實(shí)現(xiàn)簽名
- 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()函數(shù)有兩個(gè)重載簽名和一個(gè)實(shí)現(xiàn)簽名。每個(gè)重載簽名都描述了調(diào)用函數(shù)的一種方式。我們可以使用字符串參數(shù)或使用字符串參數(shù)數(shù)組來(lái)調(diào)用greet()函數(shù)。
現(xiàn)在就可以使用字符串或字符串?dāng)?shù)組的參數(shù)調(diào)用greet()):
- greet('World'); // 'Hello, World!'
- greet(['TS', 'JS']); // ['Hello, TS!', 'Hello, JS!']
在定義函數(shù)重載時(shí),需要注意以下兩點(diǎn):
(1)函數(shù)簽名是可以調(diào)用的
雖然上面我們定義了重載簽名和簽名方法,但是簽名實(shí)現(xiàn)時(shí)不能直接調(diào)用的,只有重載簽名可以調(diào)用:
- const someValue: unknown = 'Unknown';
- greet(someValue);
這樣調(diào)用的話就會(huì)報(bào)錯(cuò):
- 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[]'
也就是說(shuō),即使簽名實(shí)現(xiàn)接收unknown類型的參數(shù),但是我們不能直接給greet()方法來(lái)傳遞unknown類型的參數(shù),參數(shù)只能是函數(shù)重載簽名中定義的參數(shù)類型。
(2)實(shí)現(xiàn)簽名必須是通用的
在實(shí)現(xiàn)簽名時(shí),定義的數(shù)據(jù)類型需要是通用的,以包含重載簽名。假如我們把greet()方法的返回值類型定義為string,這時(shí)就會(huì)出問(wèn)題了:
- function greet(person: string): string;
- function greet(persons: string[]): string[];
- function greet(person: unknown): string {
- // ...
- throw new Error('error');
- }
此時(shí)string[]類型就會(huì)和string不兼容。所以,實(shí)現(xiàn)簽名的返回類型和參數(shù)類型都要包含所有重載簽名中的參數(shù)類型和返回值類型,保證是通用的。
3. 方法重載
除了常規(guī)函數(shù)外,類中的方法也可以過(guò)載,比如用重載方法greet()來(lái)實(shí)現(xiàn)一個(gè)類:
- 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()重載方法:這里面有兩個(gè)描述如何調(diào)用方法的重載簽名,以及包含其實(shí)現(xiàn)簽名。這樣我們就可以通過(guò)兩種方式調(diào)用hi.greet():
- const hi = new Greeter('Hi');
- hi.greet('World'); // 'Hello, World!'
- hi.greet(['TS', 'JS']); // ['Hello, TS!', 'Hello, JS!']