TS中的函數需要聲明參數列表和返回值的類型,除此只要,還有關於泛型、可選參數、不定長參數列表、回調函數、this、重載的聲明規則。 ...
TS官方手冊:TypeScript: Handbook - The TypeScript Handbook (typescriptlang.org)
函數類型表達式
使用類似於箭頭表達式的形式來描述一個函數的類型。
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
上述代碼中,fn: (a:string) => void
表示變數fn
是一個函數,這個函數有一個參數a
,是string
類型,且這個函數的返回值類型為void
,即沒有返回值。
調用簽名
在 JS 中,函數是對象,除了可以調用也可以擁有自己的屬性。而使用函數類型表達式無法聲明這一部分屬性的類型。
可以將函數視為一個對象,聲明一個類型,其中包含多個屬性的類型聲明,並使用調用簽名來描述函數參數和返回值的類型,取代原先函數類型表達式的寫法。
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
在這個例子中,DescribableFunction
是一個函數類型,description
是這個函數類型實例對象的一個屬性名,類型為string
。而這個函數的參數列表類型聲明為:(someArg: number)
,返回值類型為boolean
。
需要註意,在這種寫法中,參數列表和返回值類型之間是用:
隔開,而函數類型表達式是使用=>
。
構造簽名
搭配構造函數使用,在調用簽名的語法前面加上new
。
type SomeConstructor = {
new (s: string): SomeObject;
};
泛型函數
如果函數的參數類型與返回值的參數類型存在關聯,可以使用泛型:
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
// s是'string'類型
const s = firstElement(["a", "b", "c"]);
// n是'number'類型
const n = firstElement([1, 2, 3]);
// u是undefined類型
const u = firstElement([]);
多類型:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
類型約束:
泛型支持函數傳入不同的類型,當需要約束時,例如要求傳入的參數類型必須包含一個某類型的屬性,則可以:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
使用<Type extends { x : y}>
實現,extends表示繼承於類型{x:y}
,表示類型Type應該包含y
類型的屬性x
。
需要註意如果函數返回值類型為Type,那麼不能返回類型為{x:y}
,因為Type包含{x:y}
,而可能存在比{x:y}
更多的屬性。
指定類型參數:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
// 報錯,因為根據第一次參數,Type會被識別為number,但是第二個參數卻是string[]類型。
const arr = combine([1, 2, 3], ["hello"]);
如果執意這麼設計函數的話,可以考慮使用聯合類型:
const arr = combine<string | number>([1, 2, 3], ["hello"]);
使用泛型函數的建議
-
儘可能使用類型參數本身,而不去使用類型約束。
// Good: 返回值類型會被推斷為Type function firstElement1<Type>(arr: Type[]) { return arr[0]; } // Bad: 返回值類型會被推斷為any function firstElement2<Type extends any[]>(arr: Type) { return arr[0]; }
-
儘可能少地使用類型參數。
過多的類型參數會使得函數難以閱讀,儘量確保類型參數與多個值相關(例如與函數參數和返回值都有關)再使用。
// Good: 只用了Type一個類型參數 function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] { return arr.filter(func); } // Bad: Func這個類型參數是多餘的,只用在了一個函數參數 function filter2<Type, Func extends (arg: Type) => boolean>( arr: Type[], func: Func ): Type[] { return arr.filter(func); }
-
如果類型參數只出現在一個位置,那麼這個類型參數很可能不是必要的。
使用泛型是因為函數中有若幹個值的類型存在關聯,如果類型參數只出現在一個位置,很可能不是必要的。
// Bad: Str是不必要的 function greet<Str extends string>(s: Str) { console.log("Hello, " + s); } // Good function greet(s: string) { console.log("Hello, " + s); }
可選參數列表
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
註:上面的代碼中x
的類型實際為number|undefined
,當不傳入該參數的時候就是undefined
。
如果考慮設置預設值,如下,那麼x
的類型就會變成number
,排除了undefined
的情況。
function f(x = 10) {
// ...
}
註:只要一個參數是可選的,那麼這個參數就可以被傳入undefined
。
回調函數的可選參數
在設計一個回調函數的函數類型時,不要使用可選參數。
函數重載
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);
如上述代碼,先寫兩個重載函數簽名(overload signatures),然後再寫一個函數相容實現這兩個簽名,叫做實現簽名(implementation signature)。
在調用函數的時候,需要以重載簽名為標準,不能以實現簽名為標準。也就是說,上面這段代碼中的函數makeDate
,要麼傳入1個參數,要麼傳入3個參數,不能傳入2個參數。
註:
- 從外部無法看見實現的簽名。在編寫重載函數時,應該始終在函數的實現之上有兩個或多個簽名。
- 實現簽名與重載簽名之間要相容。
- 當可以使用聯合類型函數參數解決問題時,就不要使用函數重載。
聲明this的類型
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
其它與函數相關的類型
void
void作為函數的返回值類型,表示函數沒有返回值。
在 JS 中沒有返回值的函數會返回 undefined
,但是在 TS 中undefined
和void
是不同的。
當函數返回值聲明為void
,仍可以在函數體中return
內容,但不管返回了什麼值,最終接收函數返回值的那個變數都會是void
類型。
type voidFunc = ()=>void;
const f: voidFunc = ()=> true;
// 這裡value的類型會被推斷為void
const value = f();
object
object
類型是除了string
,number
,bigint
,boolean
,symbol
,null
和 undefined
的其它類型。
註:object
類型與空對象類型{}
不同,與全局類型Object
也不同。永遠不要使用Object
類型,而是使用object
。
在 JS 中,函數也是對象;在 TS 中,函數也被認為是object
類型。
unknown
unknown
和any
非常類似,但是unknown
更安全。
因為unknown
類型變數的任何操作都是非法的,這迫使大多數操作之前需要對unknown
類型變數進行類型的檢查。
而any
類型的值執行操作之前不需要進行任何檢查。
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b(); // ERROR: 'a' is of type 'unknown'.
}
註:unknown
類型只能賦值給any
或unknown
類型。
unknown
類型的意義:TS 不允許我們對類型為 unknown
的值執行任意操作。我們必須首先執行某種類型檢查以縮小我們正在使用的值的類型範圍。
可以使用類型收束(Narrowing)的操作將unknown
縮小到具體的類型,再進行後續操作。
never
never
通常描述返回值類型,表示永遠不返回值。與void
不同,使用never
意味著函數會拋出一個異常,或者程式會被終止。
function fail(msg: string): never {
throw new Error(msg);
}
另一種情況下也會出現never
,就是當聯合類型被不斷收窄到空時,就是never
:
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // 這裡x的類型是'never'
}
}
Function
全局類型Function
聲明的變數包含了 JS 中函數所擁有的所有屬性和方法,例如bind
、call
、apply
。
被Function
聲明的變數是可執行的,並且返回any
。
這種函數類型聲明方式很不安全,因為返回any
,最好使用函數類型表達式聲明:()=>void
。
function doSomething(f: Function) {
return f(1, 2, 3);
}
不定長參數列表
在 TS 中,不定長參數列表的類型應該被聲明為Array<T>
、T[]
或元組類型。
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
spread語法可以展開可迭代對象(例如數組,對象)變成不定長的參數列表。例如push
函數可以接收多個參數。
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
但需要註意,TS 認為數組是可變的,數組長度是可變的。
觀察下麵的案例代碼:
const args = [8, 5];
const angle = Math.atan2(...args);
儘管Math.atan2
接收兩個number
類型的參數,而args
也剛好是長度為2的number[]
類型數組,展開後剛好。
但是這段代碼會報錯,因為數組是可變的。
一種較為直接的解決方法是使用const
:
// 視為長度為2的元組
const args = [8, 5] as const;
// 現在不會報錯了
const angle = Math.atan2(...args);
參數解構的類型聲明
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
或者使用type
簡化:
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}