更多函數

函式是任何應用程式的基本建構模組,無論是本機函式、從其他模組匯入的函式,還是類別上的方法。它們也是值,就像其他值一樣,TypeScript 有許多方法來描述如何呼叫函式。讓我們來瞭解如何撰寫描述函式的類型。

函式類型表達式

描述函數最簡單的方式是使用函數類型表達式。這些類型在語法上與箭頭函數類似

ts
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
 
function printToConsole(s: string) {
console.log(s);
}
 
greeter(printToConsole);
Try

語法 (a: string) => void 表示「一個函數,有一個參數,名稱為 a,類型為 string,沒有回傳值」。就像函數宣告一樣,如果未指定參數類型,則預設為 any

請注意,參數名稱是必需的。函數類型 (string) => void 表示「一個函數,有一個參數,名稱為 string,類型為 any」!

當然,我們可以使用類型別名來命名函數類型

ts
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
Try

呼叫簽章

在 JavaScript 中,函數除了可以呼叫之外,還可以有屬性。然而,函數類型表達式語法不允許宣告屬性。如果我們想要描述具有屬性的可呼叫項目,我們可以在物件類型中撰寫呼叫簽章

ts
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
 
function myFunc(someArg: number) {
return someArg > 3;
}
myFunc.description = "default description";
 
doSomething(myFunc);
Try

請注意,與函數類型表達式相比,語法略有不同 - 在參數清單和回傳類型之間使用 :,而不是 =>

建構簽章

JavaScript 函數也可以使用 new 算子呼叫。TypeScript 將它們稱為建構函數,因為它們通常會建立一個新物件。您可以在呼叫簽章前面加上 new 關鍵字來撰寫建構簽章

ts
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
Try

某些物件,例如 JavaScript 的 Date 物件,可以呼叫或不呼叫 new。您可以任意在同一個類型中組合呼叫和建構簽章

ts
interface CallOrConstruct {
(n?: number): string;
new (s: string): Date;
}
Try

泛型函數

通常會撰寫一個函數,其中輸入的類型與輸出的類型相關,或兩個輸入的類型在某種程度上相關。讓我們暫時考慮一個傳回陣列第一個元素的函數

ts
function firstElement(arr: any[]) {
return arr[0];
}
Try

此函數完成了它的工作,但不幸的是回傳類型為 any。如果函數傳回陣列元素的類型會更好。

在 TypeScript 中,當我們想要描述兩個值之間的對應關係時,會使用泛型。我們透過在函式簽章中宣告類型參數來執行此動作

ts
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
Try

透過將類型參數 Type 加入這個函式並在兩個地方使用它,我們在函式的輸入(陣列)和輸出(傳回值)之間建立一個連結。現在當我們呼叫它時,會產生更具體的類型

ts
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
Try

推論

請注意,我們不必在這個範例中指定 Type。類型會由 TypeScript 自動推論(選擇)。

我們也可以使用多個類型參數。例如,獨立版本的 map 會像這樣

ts
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
 
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
Try

請注意,在這個範例中,TypeScript 可以推論 Input 類型參數(從給定的 string 陣列)的類型,以及根據函式表達式的傳回值(number)推論 Output 類型參數的類型。

約束

我們寫了一些泛型函式,它們可以在任何類型的值上運作。有時我們想要關聯兩個值,但只能在特定值子集中運作。在這種情況下,我們可以使用約束來限制類型參數可以接受的類型種類。

我們來寫一個傳回兩個值中較長者的函式。為此,我們需要一個長度為數字的 length 屬性。我們透過撰寫 extends 子句來約束類型參數為該類型

ts
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
 
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.2345Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
Try

在這個範例中,有幾件有趣的事情需要注意。我們允許 TypeScript 推論 longest 的傳回類型。傳回類型推論也適用於泛型函式。

由於我們將 Type 約束為 { length: number },因此我們可以存取 ab 參數的 .length 屬性。如果沒有類型約束,我們將無法存取這些屬性,因為這些值可能是沒有長度屬性的其他類型。

longerArraylongerString 的類型是根據引數推論出來的。請記住,泛型就是將兩個或多個具有相同類型的值關聯起來!

最後,正如我們所希望的,對 longest(10, 100) 的呼叫會被拒絕,因為 number 類型沒有 .length 屬性。

使用受約束值

以下是使用泛型約束時常見的錯誤

ts
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.2322Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
Try

這個函式看起來沒問題 - Type 約束為 { length: number },而且函式會傳回 Type 或符合該約束的值。問題在於函式承諾會傳回與傳入的物件相同類型的物件,而不是僅傳回符合約束的某些物件。如果這段程式碼是合法的,你可以撰寫絕對無法運作的程式碼

ts
// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));
Try

指定類型引數

TypeScript 通常可以推論泛型呼叫中的預期類型引數,但並非總是如此。例如,假設你撰寫了一個函式來合併兩個陣列

ts
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
Try

通常,使用不匹配的陣列呼叫此函式會產生錯誤

ts
const arr = combine([1, 2, 3], ["hello"]);
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

不過,如果您打算這樣做,您可以手動指定 Type

ts
const arr = combine<string | number>([1, 2, 3], ["hello"]);
Try

撰寫良好泛型函式的準則

撰寫泛型函式很有趣,而且很容易沉迷於類型參數。類型參數太多或在不需要的地方使用約束可能會降低推論的成功率,讓函式呼叫者感到沮喪。

向下推類型參數

以下是撰寫看似類似的函式的兩種方式

ts
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
 
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
 
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
Try

乍看之下,這些函式可能看起來相同,但 firstElement1 是撰寫此函式更好的方式。其推論的回傳類型為 Type,但 firstElement2 的推論回傳類型為 any,因為 TypeScript 必須使用約束類型來解析 arr[0] 表達式,而不是在呼叫期間「等待」解析元素。

規則:如果可能,請使用類型參數本身,而不是約束它

使用較少的類型參數

以下是另一對類似的函式

ts
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
 
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
Try

我們建立了一個類型參數 Func,它不關聯兩個值。這總是一個警訊,因為這表示想要指定類型參數的呼叫者必須無緣無故手動指定額外的類型參數。Func 除了讓函式更難閱讀和理解之外,什麼事都不做!

規則:盡可能使用較少的類型參數

類型參數應出現兩次

有時我們會忘記一個函式可能不需要是泛型的

ts
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
 
greet("world");
Try

我們可以很輕易地寫出一個更簡單的版本

ts
function greet(s: string) {
console.log("Hello, " + s);
}
Try

請記住,類型參數用於關聯多個值的類型。如果類型參數只在函式簽章中使用一次,它就不會關聯任何東西。這包括推斷的回傳類型;例如,如果 Strgreet 推斷的回傳類型的一部分,它將關聯參數和回傳類型,因此會被使用兩次,儘管在寫好的程式碼中只出現一次。

規則:如果類型參數只出現在一個位置,請仔細考慮你是否真的需要它

選用參數

JavaScript 中的函式通常會採用變數個數的參數。例如,numbertoFixed 方法會採用一個選用的位數計數

ts
function f(n: number) {
console.log(n.toFixed()); // 0 arguments
console.log(n.toFixed(3)); // 1 argument
}
Try

我們可以在 TypeScript 中透過使用 ? 將參數標記為選用來建模這個方法

ts
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
Try

儘管參數指定為類型 number,但 x 參數實際上會有類型 number | undefined,因為 JavaScript 中未指定的參數會取得值 undefined

你也可以提供參數預設值

ts
function f(x = 10) {
// ...
}
Try

現在在 f 的主體中,x 會有類型 number,因為任何 undefined 參數都會被替換為 10。請注意,當參數是選用的,呼叫者總是能傳遞 undefined,因為這只是模擬一個「遺失」的參數

ts
// All OK
f();
f(10);
f(undefined);
Try

選用參數在呼叫回函

一旦你了解選用參數和函式類型表達式後,在撰寫呼叫呼叫回函的函式時,很容易發生下列錯誤

ts
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
Try

人們通常在撰寫 index? 作為選用參數時,希望這兩個呼叫都是合法的

ts
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));
Try

實際上表示callback 可能會呼叫一個參數。換句話說,函式定義表示實作可能如下所示

ts
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// I don't feel like providing the index today
callback(arr[i]);
}
}
Try

反過來,TypeScript 將強制執行此含義,並發出實際上不可能發生的錯誤

ts
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed());
'i' is possibly 'undefined'.18048'i' is possibly 'undefined'.
});
Try

在 JavaScript 中,如果你呼叫函式時參數多於參數數量,額外的參數將會被忽略。TypeScript 的行為相同。參數較少的函式(類型相同)永遠可以取代參數較多的函式。

規則:撰寫呼叫回函的函式類型時,絕不撰寫選用參數,除非你打算呼叫函式而不傳遞該參數

函式超載

有些 JavaScript 函式可以在各種參數計數和類型中呼叫。例如,你可以撰寫一個函式來產生 Date,它可以接受時間戳記(一個參數)或月份/日期/年份規格(三個參數)。

在 TypeScript 中,我們可以透過撰寫超載簽章來指定一個函式,它可以用不同的方式呼叫。為此,請撰寫一些函式簽章(通常是兩個或更多),然後是函式主體

ts
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.2575No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
Try

在此範例中,我們撰寫了兩個超載:一個接受一個參數,另一個接受三個參數。這前兩個簽章稱為超載簽章

然後,我們撰寫了一個具有相容簽章的函式實作。函式有一個實作簽章,但無法直接呼叫此簽章。即使我們在必需參數後撰寫了一個具有兩個選用參數的函式,但它無法呼叫兩個參數!

重載簽章和實作簽章

這是常見的混淆來源。人們經常會寫出像這樣的程式碼,卻不明白為什麼會出錯

ts
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.2554Expected 1 arguments, but got 0.
Try

同樣地,用來撰寫函式主體的簽章無法從外部「看到」。

實作 的簽章從外部不可見。在撰寫重載函式時,函式實作上方應該永遠有 兩個 或以上的簽章。

實作簽章也必須與重載簽章相容。例如,以下函式有錯誤,因為實作簽章與重載沒有正確對應

ts
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
This overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
Try
ts
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
This overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.
function fn(x: string | number) {
return "oops";
}
Try

撰寫良好的重載

與泛型一樣,在使用函式重載時,您應該遵循一些準則。遵循這些原則將使您的函式更容易呼叫、更容易理解,並且更容易實作。

讓我們考慮一個傳回字串或陣列長度的函式

ts
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
Try

這個函式很好;我們可以用字串或陣列呼叫它。但是,我們無法使用可能是字串陣列的值呼叫它,因為 TypeScript 只可以將函式呼叫解析為單一重載

ts
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.2769No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.
Try

由於兩個重載都有相同的參數數量和相同的回傳類型,我們可以改寫成非重載版本的函式

ts
function len(x: any[] | string) {
return x.length;
}
Try

這樣好多了!呼叫者可以使用任一種值呼叫此函式,而且額外的好處是,我們不必找出正確的實作簽章。

如果可以,請務必優先使用具有聯集類型的參數,而不是重載

在函式中宣告 this

TypeScript 會透過程式碼流程分析來推論函式中的 this 應該是什麼,例如以下範例

ts
const user = {
id: 123,
 
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
Try

TypeScript 了解函式 user.becomeAdmin 有對應的 this,也就是外部物件 userthis 對於許多情況來說已經足夠了,但有許多情況需要更進一步控制 this 所代表的物件。JavaScript 規範指出您不能有一個稱為 this 的參數,因此 TypeScript 使用該語法空間讓您在函式主體中宣告 this 的類型。

ts
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
Try

這種模式常見於回呼式 API,其中另一個物件通常會控制您的函式何時被呼叫。請注意,您需要使用 function 而不是箭頭函式才能獲得此行為

ts
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin);
The containing arrow function captures the global value of 'this'.
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
7041
7017
The containing arrow function captures the global value of 'this'.
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
Try

其他需要了解的類型

在使用函式類型時,有一些額外的類型您會想要認識,它們經常出現。與所有類型一樣,您可以在任何地方使用它們,但這些類型在函式的脈絡中特別相關。

void

void 代表不回傳值的函式的回傳值。當函式沒有任何 return 陳述式,或從這些回傳陳述式回傳任何明確的值時,它就是推論的型別。

ts
// The inferred return type is void
function noop() {
return;
}
Try

在 JavaScript 中,不回傳任何值的函式會隱含回傳 undefined 值。然而,在 TypeScript 中,voidundefined 並不相同。本章節的最後面有進一步的詳細資訊。

voidundefined 不同。

object

特殊型別 object 指涉任何非原生的值(stringnumberbigintbooleansymbolnullundefined)。這與空物件型別 { } 不同,也與全域型別 Object 不同。您很可能永遠不會使用 Object

object 不是 Object始終使用 object

請注意,在 JavaScript 中,函式值是物件:它們有屬性,在它們的原型鏈中有 Object.prototype,是 instanceof Object,您可以在它們上呼叫 Object.keys,等等。因此,函式型別在 TypeScript 中被視為 object

unknown

unknown 型別代表任何值。這類似於 any 型別,但更安全,因為無法對 unknown 值執行任何操作。

ts
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
'a' is of type 'unknown'.18046'a' is of type 'unknown'.
}
Try

在描述函式型別時,這很有用,因為您可以在函式主體中沒有 any 值的情況下描述接受任何值的函式。

相反地,您可以描述傳回未知型別值的函式

ts
function safeParse(s: string): unknown {
return JSON.parse(s);
}
 
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);
Try

never

某些函數永遠不會傳回值

ts
function fail(msg: string): never {
throw new Error(msg);
}
Try

never 類型代表永遠不會觀察到的值。在傳回類型中,這表示函數會擲回例外或終止程式執行。

當 TypeScript 判斷聯集內沒有任何內容時,也會出現 never

ts
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
Try

Function

全域類型 Function 描述 JavaScript 中所有函數值上存在的屬性,例如 bindcallapply 等。它還有一個特殊屬性,即類型為 Function 的值總是可呼叫的;這些呼叫會傳回 any

ts
function doSomething(f: Function) {
return f(1, 2, 3);
}
Try

這是一個未輸入型別的函數呼叫,由於不安全的 any 傳回類型,通常最好避免使用。

如果您需要接受任意函數,但無意呼叫它,則類型 () => void 通常較安全。

Rest 參數和引數

背景讀物
Rest 參數
展開語法

Rest 參數

除了使用選用參數或重載來建立可接受各種固定引數數量的函數外,我們還可以定義使用rest 參數來取得無界數量的引數的函數。

休息參數出現在所有其他參數之後,並使用 ... 語法

ts
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
Try

在 TypeScript 中,這些參數的類型註解隱含為 any[],而不是 any,並且給定的任何類型註解都必須為 Array<T>T[] 形式,或元組類型(我們稍後會學習)。

休息參數

相反地,我們可以使用擴散語法從可迭代物件(例如陣列)提供變數數量的參數。例如,陣列的 push 方法會接收任意數量的參數

ts
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
Try

請注意,一般來說,TypeScript 不會假設陣列是不可變的。這可能會導致一些令人驚訝的行為

ts
// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
A spread argument must either have a tuple type or be passed to a rest parameter.2556A spread argument must either have a tuple type or be passed to a rest parameter.
Try

這種情況的最佳修復方式取決於您的程式碼,但一般來說,const 環境是最直接的解決方案

ts
// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);
Try

使用休息參數可能需要在目標為舊執行時期時開啟 downlevelIteration

參數解構

背景讀物
解構指定

您可以使用參數解構來方便地將提供為參數的物件解壓縮到函式主體中的一個或多個局部變數中。在 JavaScript 中,它看起來像這樣

js
function sum({ a, b, c }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });

物件的類型註解出現在解構語法之後

ts
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
Try

這看起來可能有點冗長,但您也可以在此處使用命名類型

ts
// Same as prior example
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
Try

函式的可指派性

ts

而當其中一個函數的回傳值指定給另一個變數時,它會保留 void 的類型

ts
const v1 = f1();
 
const v2 = f2();
 
const v3 = f3();
Try

此行為的存在是因為以下程式碼是有效的,即使 Array.prototype.push 回傳數字,而 Array.prototype.forEach 方法預期函數的回傳類型為 void

ts
const src = [1, 2, 3];
const dst = [0];
 
src.forEach((el) => dst.push(el));
Try

還有一個特殊情況需要注意,當文字函數定義具有 void 回傳類型時,該函數不得回傳任何東西。

ts
function f2(): void {
// @ts-expect-error
return true;
}
 
const f3 = function (): void {
// @ts-expect-error
return true;
};
Try

有關 void 的更多資訊,請參閱這些其他文件條目

TypeScript 文件是一個開放原始碼專案。透過 傳送 Pull Request ❤ 來協助我們改善這些頁面

此頁面的貢獻者
RCRyan Cavanaugh (56)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (4)
JWJoseph Wynn (3)
SBSiarhei Bobryk (2)
34+

最後更新:2024 年 3 月 21 日