this
作為參數(進階)本章探討 TypeScript 中函數的靜態輸入。
在本章中,「函數」意指「函數或方法或建構函數」
在本章中,關於函數的大部分說明(特別是關於參數處理),也適用於方法和建構函數。
這是 TypeScript 中函式宣告的範例
function repeat1(str: string, times: number): string { // (A)
.repeat(times);
return str
}.equal(
assertrepeat1('*', 5), '*****');
參數:如果編譯器選項 --noImplicitAny
已開啟(如果 --strict
已開啟,則會開啟),則每個參數的類型必須可以推論或明確指定。(我們稍後會仔細了解推論。)在此情況下,無法推論,因此 str
和 times
有類型註解。
傳回值:預設情況下,函式的傳回類型會推論。這通常已經夠好。在此情況下,我們選擇明確指定 repeat1()
的傳回類型為 string
(A 行中的最後一個類型註解)。
repeat1()
的箭頭函式版本如下所示
= (str: string, times: number): string => {
const repeat2 .repeat(times);
return str; }
在此情況下,我們也可以使用表達式主體
= (str: string, times: number): string =>
const repeat3 .repeat(times); str
我們可以透過函式類型簽章定義函式的類型
type Repeat = (str: string, times: number) => string;
此函式類型的名稱為 Repeat
。其中,它符合所有函式,其
string
和 number
。我們需要在函式類型簽章中命名參數,但在檢查兩個函式類型是否相容時,會忽略這些名稱。string
。請注意,這次類型以箭頭分隔,且不能省略。此類型符合更多函式。我們將在本章稍後探討 可指派性 規則時,了解哪些函式符合。
我們也可以使用介面定義函式類型
interface Repeat {: string, times: number): string; // (A)
(str }
注意
一方面,介面較為詳細。另一方面,它們讓我們可以指定函式的屬性(這很少見,但確實會發生)
interface Incrementor1 {: number): number;
(x: number;
increment }
我們也可以透過函數簽章類型和物件字面類型交集類型 (&
) 來指定屬性
type Incrementor2 =
: number) => number
(x& { increment: number }
;
舉例來說,考慮以下場景:函式庫匯出下列函數類型。
type StringPredicate = (str: string) => boolean;
我們想要定義一個函數,其類型與 StringPredicate
相容。而且我們想要立即檢查是否確實如此(與在我們第一次使用它時才發現不同)。
如果我們透過 const
宣告變數,我們可以透過類型註解來執行檢查
: StringPredicate = (str) => str.length > 0; const pred1
請注意,我們不需要指定參數 str
的類型,因為 TypeScript 可以使用 StringPredicate
來推斷它。
檢查函數宣告比較複雜
function pred2(str: string): boolean {
.length > 0;
return str
}
// Assign the function to a type-annotated variable
: StringPredicate = pred2; const pred2ImplementsStringPredicate
下列解決方案有點過頭(也就是說,如果你無法完全理解,不用擔心),但它展示了幾個進階功能
function pred3(...[str]: Parameters<StringPredicate>)
: ReturnType<StringPredicate> {
.length > 0;
return str }
參數:我們使用 Parameters<>
來萃取一個具有參數類型的元組。三個點宣告一個 rest 參數,它會將所有參數收集到一個元組/陣列中。[str]
解構那個元組。(本章稍後會進一步說明 rest 參數。)
傳回值:我們使用 ReturnType<>
來萃取傳回類型。
回顧:如果開啟 --noImplicitAny
(--strict
會開啟它),每個參數的類型都必須可以推斷或明確指定。
在下列範例中,TypeScript 無法推斷 str
的類型,我們必須指定它
function twice(str: string) {
+ str;
return str }
在 A 行,TypeScript 可以使用類型 StringMapFunction
來推斷 str
的類型,我們不需要加入類型註解
type StringMapFunction = (str: string) => string;
: StringMapFunction = (str) => str + str; // (A) const twice
在此,TypeScript 可以使用 .map()
的類型來推斷 str
的類型
.deepEqual(
assert'a', 'b', 'c'].map((str) => str + str),
['aa', 'bb', 'cc']); [
這是 .map()
的類型
<T> {
interface Arraymap<U>(
: (value: T, index: number, array: T[]) => U,
callbackfn?: any
thisArg: U[];
)// ···
}
在本節中,我們將探討幾種允許省略參數的方法。
str?: string
如果我們在參數名稱後加上問號,該參數就會變成可選的,而且在呼叫函數時可以省略
function trim1(str?: string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim1:
// %inferred-type: (str?: string | undefined) => string
; trim1
以下是 trim1()
可以如何被呼叫
.equal(
asserttrim1('\n abc \t'), 'abc');
.equal(
asserttrim1(), '');
// `undefined` is equivalent to omitting the parameter
.equal(
asserttrim1(undefined), '');
str: undefined|string
在外部,trim1()
的參數 str
具有類型 string|undefined
。因此,trim1()
大多等同於下列函數。
function trim2(str: undefined|string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim2:
// %inferred-type: (str: string | undefined) => string
; trim2
trim2()
與 trim1()
不同的唯一方式是,在函數呼叫中無法省略參數(A 行)。換句話說:當省略類型為 undefined|T
的參數時,我們必須明確表示。
.equal(
asserttrim2('\n abc \t'), 'abc');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
trim2(); // (A)
.equal(
asserttrim2(undefined), ''); // OK!
str = ''
如果我們為 str
指定參數預設值,我們不需要提供型別註解,因為 TypeScript 可以推斷型別
function trim3(str = ''): string {
// Internal type of str:
// %inferred-type: string
;
str
.trim();
return str
}
// External type of trim2:
// %inferred-type: (str?: string) => string
; trim3
請注意,str
的內部型別為 string
,因為預設值確保它永遠不會是 undefined
。
讓我們呼叫 trim3()
.equal(
asserttrim3('\n abc \t'), 'abc');
// Omitting the parameter triggers the parameter default value:
.equal(
asserttrim3(), '');
// `undefined` is allowed and triggers the parameter default value:
.equal(
asserttrim3(undefined), '');
我們也可以同時指定型別和預設值
function trim4(str: string = ''): string {
.trim();
return str }
Rest 參數會將所有剩餘參數收集到陣列中。因此,它的靜態型別通常是陣列。在以下範例中,parts
是 Rest 參數
function join(separator: string, ...parts: string[]) {
.join(separator);
return parts
}.equal(
assertjoin('-', 'state', 'of', 'the', 'art'),
'state-of-the-art');
下一個範例展示兩個功能
[string, number]
,作為 Rest 參數。function repeat1(...[str, times]: [string, number]): string {
.repeat(times);
return str }
repeat1()
等同於以下函式
function repeat2(str: string, times: number): string {
.repeat(times);
return str }
命名參數 是 JavaScript 中的熱門模式,其中物件文字用於為每個參數命名。如下所示
.equal(
assertpadStart({str: '7', len: 3, fillStr: '0'}),
'007');
在純 JavaScript 中,函式可以使用解構來存取命名參數值。遺憾的是,在 TypeScript 中,我們還必須為物件文字指定型別,這會導致重複
function padStart({ str, len, fillStr = ' ' } // (A)
: { str: string, len: number, fillStr: string }) { // (B)
.padStart(len, fillStr);
return str }
請注意,解構(包括 fillStr
的預設值)全部發生在 A 行,而 B 行專門用於 TypeScript。
可以定義一個單獨的型別,而不是我們在 B 行中使用的內嵌物件文字型別。然而,在大多數情況下,我寧願不這樣做,因為這稍微違背了參數的本質,而參數是每個函式中局部且唯一的。如果你希望函式標題中內容較少,那也沒關係。
this
作為參數(進階)每個常規函式總是有隱式參數 this
,這使它能夠在物件中用作方法。有時我們需要為 this
指定型別。對於此用例,有僅限於 TypeScript 的語法:常規函式的參數之一可以命名為 this
。這種參數只存在於編譯時,並在執行時消失。
舉例來說,考慮以下 DOM 事件來源介面(略微簡化的版本)
interface EventSource {addEventListener(
: string,
type: (this: EventSource, ev: Event) => any,
listener?: boolean | AddEventListenerOptions
options: void;
)// ···
}
回呼 listener
的 this
永遠是 EventSource
的執行個體。
下一個範例展示 TypeScript 如何使用 this
參數提供的型別資訊來檢查 .call()
的第一個引數(A 行和 B 行)
function toIsoString(this: Date): string {
.toISOString();
return this
}
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'Date'. (2345)
.throws(() => toIsoString.call('abc')); // (A) error
assert
.call(new Date()); // (B) OK toIsoString
此外,我們無法呼叫 toIsoString()
作為物件 obj
的方法,因為它的接收者不是 Date
的執行個體
= { toIsoString };
const obj // @ts-expect-error: The 'this' context of type
// '{ toIsoString: (this: Date) => string; }' is not assignable to
// method's 'this' of type 'Date'. [...]
.throws(() => obj.toIsoString()); // error
assert.toIsoString.call(new Date()); // OK obj
有時單一型別簽章無法充分描述函式的運作方式。
考慮函式 getFullName()
,我們在以下範例中呼叫它(A 行和 B 行)
interface Customer {: string;
id: string;
fullName
}= {id: '1234', fullName: 'Jane Bond'};
const jane = {id: '5678', fullName: 'Lars Croft'};
const lars = new Map<string, Customer>([
const idToCustomer '1234', jane],
['5678', lars],
[;
])
.equal(
assertgetFullName(idToCustomer, '1234'), 'Jane Bond'); // (A)
.equal(
assertgetFullName(lars), 'Lars Croft'); // (B)
我們要如何實作 getFullName()
?以下實作適用於先前範例中的兩個函式呼叫
function getFullName(
: Customer | Map<string, Customer>,
customerOrMap?: string
id: string {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
然而,使用這個型別簽章,在編譯時合法的函式呼叫會產生執行時期錯誤
.throws(() => getFullName(idToCustomer)); // missing ID
assert.throws(() => getFullName(lars, '5678')); // ID not allowed assert
以下程式碼修正了這些問題
function getFullName(customerOrMap: Customer): string; // (A)
function getFullName( // (B)
: Map<string, Customer>, id: string): string;
customerOrMapfunction getFullName( // (C)
: Customer | Map<string, Customer>,
customerOrMap?: string
id: string {
)// ···
}
// @ts-expect-error: Argument of type 'Map<string, Customer>' is not
// assignable to parameter of type 'Customer'. [...]
getFullName(idToCustomer); // missing ID
// @ts-expect-error: Argument of type '{ id: string; fullName: string; }'
// is not assignable to parameter of type 'Map<string, Customer>'.
// [...]
getFullName(lars, '5678'); // ID not allowed
這裡發生了什麼事?getFullName()
的型別簽章被覆寫了
getFullName()
使用。實際實作的型別簽章無法使用!我的建議是僅在無法避免時才使用覆寫。一種替代方案是將覆寫的函式拆分為具有不同名稱的多個函式,例如
getFullName()
getFullNameViaMap()
在介面中,我們可以有多個不同的呼叫簽章。這讓我們能夠在以下範例中使用介面 GetFullName
進行覆寫
interface GetFullName {: Customer): string;
(customerOrMap: Map<string, Customer>, id: string): string;
(customerOrMap
}
: GetFullName = (
const getFullName: Customer | Map<string, Customer>,
customerOrMap?: string
id: string => {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
在以下範例中,我們覆寫並使用字串文字型別(例如 'click'
)。這讓我們可以根據參數 type
的值來變更參數 listener
的型別
function addEventListener(elem: HTMLElement, type: 'click',
: (event: MouseEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: 'keypress',
: (event: KeyboardEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: string, // (A)
: (event: any) => void): void {
listener.addEventListener(type, listener); // (B)
elem }
在這種情況下,要正確取得實作的型別(從 A 行開始)並讓主體中的陳述式(B 行)運作,相對困難。作為最後的手段,我們可以隨時使用型別 any
。
以下範例示範方法的覆寫:方法 .add()
已被覆寫。
class StringBuilder {= '';
#data
add(num: number): this;
add(bool: boolean): this;
add(str: string): this;
add(value: any): this {
.#data += String(value);
this;
return this
}
toString() {
.#data;
return this
}
}
= new StringBuilder();
const sb
sb.add('I can see ')
.add(3)
.add(' monkeys!')
;
.equal(
assert.toString(), 'I can see 3 monkeys!') sb
Array.from()
的型別定義是覆寫介面方法的一個範例
interface ArrayConstructor {from<T>(arrayLike: ArrayLike<T>): T[];
from<T, U>(
: ArrayLike<T>,
arrayLike: (v: T, k: number) => U,
mapfn?: any
thisArg: U[];
) }
在第一個簽章中,傳回的陣列與參數具有相同的元素型別。
在第二個簽章中,傳回陣列的元素與 mapfn
的結果具有相同的型別。此版本的 Array.from()
類似於 Array.prototype.map()
。
在本節中,我們探討 可指派性 的型別相容性規則:型別為 Src
的函式可以傳輸到型別為 Trg
的儲存位置(變數、物件屬性、參數等)嗎?
了解可指派性有助於我們回答下列問題
在本小節中,我們探討可指派性的通用規則(包括函式的規則)。在下一個小節中,我們探討這些規則對函式的意義。
類型 Src
可指派給類型 Trg
,如果符合下列其中一個條件
Src
和 Trg
是相同的類型。Src
或 Trg
是 any
類型。Src
是字串文字類型,而 Trg
是原始類型字串。Src
是聯集類型,而 Src
的每個組成類型都可指派給 Trg
。Src
和 Trg
是函式類型,而且
Trg
有 rest 參數,或 Src
的必要參數數量小於或等於 Trg
的參數總數。Trg
中的每個參數類型都可指派給 Src
中對應的參數類型。Trg
的回傳類型是 void
,或 Src
的回傳類型可指派給 Trg
的回傳類型。在本小節中,我們探討指派規則對下列兩個函式 targetFunc
和 sourceFunc
的意義
: Trg = sourceFunc; const targetFunc
範例
: (x: RegExp) => Object = (x: Object) => /abc/; const trg1
以下範例示範,如果目標回傳類型為 void
,那麼來源回傳類型並不重要。為什麼?在 TypeScript 中,void
結果總是會被忽略。
: () => void = () => new Date(); const trg2
來源參數不可多於目標參數
// @ts-expect-error: Type '(x: string) => string' is not assignable to
// type '() => string'. (2322)
: () => string = (x: string) => 'abc'; const trg3
來源參數可少於目標參數
: (x: string) => string = () => 'abc'; const trg4
為什麼?目標會指定來源的預期:它必須接受參數 x
。它確實接受(但會忽略)。這種寬容性允許
'a', 'b'].map(x => x + x) [
.map()
的 callback 僅有一個參數,而 .map()
的類型簽章中提到了三個參數
map<U>(
: (value: T, index: number, array: T[]) => U,
callback?: any
thisArg: U[]; )