閱讀今天的文章前,先回顧一下昨天的學習,回答看看:
- any 和 unknown 型別的差別為何?
- 倘若希望 TypeScript 編譯時禁止隱性推論 any 型別,要如何做設定呢?
如果有點不清楚答案的話,可以看看 Day16 的文章喔!
通用型別(Generics Types)是指在定義宣告時不預先指定具體的型別,而是執行的時候才確認型別的特殊方式,經常用在函式、介面或類別等型別。
舉例來說,下面的 foo 函式會回傳和參數同樣型別的回傳值,但僅限數字型別。
function foo(arg: number): number{
return arg;
}
但如果我們希望傳入的參數可以是多種型別呢? 使用函式重載似乎可以做到。
function foo(arg: number):number{
return arg;
}
function foo(arg: string):string{
return arg;
}
function foo(arg: boolean):boolean{
return arg;
}
但列出列出參數的所有可能型別實在太長了,所以也不適合!那使用 any 型別呢?
function foo(arg: any): any{
return arg;
}
的確,使用 any 型別就可以讓函式接收任何型別的參數,但卻無法達到傳入型別與回傳型別一致的目標,因為,回傳值也可以是任何型別。
這時候,我們需要一種新的方法來做到這件事,這就是通用型別(Generic Types)
出現的原因!
function foo<T>(arg: T): T{
return arg;
}
上面這段程式碼使用了型別變數(type variable),在函式後面加上<T>
,其中 T 用來指代任意輸入的型別,之後就可以使用 T 作為回傳值同型別的指代。倘若參數是字串型別,則回傳值亦為字串型別 ; 倘若參數是布林型別,則回傳值亦為布林型別。如此,就可以達到原本的目標,讓傳入參數和回傳值型別相同 T => T,且傳入型別可以是任何型別。
型別變數 T ,又稱為型別參數(type parameters)和通用參數(generic parameters),有下面幾個特性:
來試著創建一個通用函式並執行,執行函式時可以只傳函式參數(型別參數 TS 會自動做型別推論),當然也可以傳函式參數加型別參數
function foo<T>(arg: T): T {
return arg;
}
// 傳遞型別參數
foo<number>
// 傳遞函式參數(自動判斷型別參數)
foo(1);
// 傳遞函式參數及型別參數
foo<number>(1);
要注意的是,在一些複雜的情況下,可能只能使用型別註記。
除了上面提及的單一參數外,通用型別函式也可以傳入多個參數
function exchange<T, U, V>(tuple: [T, U, V]): [U, T, V] {
return [tuple[1], tuple[0], tuple[2]];
}
exchange([18, 'hello',true]); // ['hello', 18, true]
//若型別重複也沒關係
exchange([18, 'hello','world']) //['hello', 18, 'world']
型別變數常用T、U、V 表示,倘若參數中不只有一個通用型別,可以使用較語義化名稱。
在函式內部使用通用變數時,由於無法事前知道傳進的參數型別,因此,也無法任意操作其屬性或方法
function foo<T>(arg: T): T{
console.log(arg.length) //Property 'length' does not exist on type 'T'
return arg;
}
T 代表的是任意型別,T 有可能是字串、數字或其他型別,因此不一定包含length屬性,這時 TS 編譯就會報錯。
倘若我們直接操作 T 型別的陣列,而非直接操作 T ,因為是陣列,length屬性就可以使用
function foo<T>(arg: T[]):T[]{
console.log(arg.length) //OK
return arg
}
//也可以寫成
function foo<T>(arg: Array<T>):Array<T>{
console.log(arg.length) //OK
return arg
}
通用函式和非通用函式的型別註記沒有差很多,只是通用函式會有一個型別參數在最前面
// 普通函式
let foo: (arg: string) => string =
function(arg: string): string {
return arg;
};
// 通用函式
let foo: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
當然,型別參數名也可以換成其他
let foo: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
除了函式會常使用通用型別外,介面(Interface)和類別(Class)也會很常看見通用型別。帶有通用型別的介面就稱為通用介面。
介面和類別之後會在詳細介紹,今天的文章僅稍微帶過和通用型別有關部分的介面和類別。
究竟什麼是介面(Interface)呢?介面可以想像成是轉接頭,在不同國家旅行的時候,要使用符合插孔形狀的轉接頭才可以使用,介面(Interface)就是用來定義需要符合的形狀(型別格式),舉例來說:
// 定義介面
interface Person{
lastname: string,
firstname: string,
age: number
}
//完全符合,通過
const martin : Person = {
lastname:'Liu',
firstname: 'Martin',
age: 18
}
// 少了一個屬性 => 報錯
const una: Person = {
lastname: 'Lin',
age: 20
}
// 多了一個屬性 => 報錯
const peter: Person = {
lastname: 'Yang',
firstname: 'Peter',
age: 33,
hasPet: true
}
// 屬性對應型別錯誤 => 報錯
const paul: Person = {
lastname: 'Wu',
firstname: 'Paul',
age: '30',
}
初步認識介面之後,我們就可以使用介面來處理上面使用通用型別,但無法使用屬性或方法的問題。在通用型別中,若要使用型別的屬性或方法,需要進行通用型別限縮(Generic Constraints),而介面是其中一種方法,舉例來說:
interface foodie{
length: number
}
function foo<T extends foodie>(arg: T):T{
console.log(arg.length)
return arg
}
這裡使用 extends 關鍵字讓 T 為 foodie 的擴展,限縮了 T 的型別,讓 T 不再適用於任何型別,如此,TS 在編譯時就不會報錯了!
現在這個通用函式被限縮了型別,傳入的參數就必須包含介面中設定的屬性
foo(3) // Error: Argument of type '3' is not assignable to parameter of type 'foodie'
foo({length:10}) // 10
// 若傳入多的參數則沒關係
foo({length:10, name:'Kira'})
在通用型別中,也可以讓型別參數被另一個型別參數給限縮,舉例來說:我們希望傳入函式的其中一個參數-屬性名稱一定要存在某物件,就可以這樣寫:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // OK
getProperty(x, "m"); // Error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
第一個參數傳入物件 x ,而第二個參數傳入 key ,而 key因為是物件 T key值的擴展,因此,只能傳入a、b、c 或 d 。
和通用介面類似,通用型別也能用在類別(class)的創建,例如:
class Log <T>{
run(value: T) {
console.log(value);
return value
}
}
let log1 = new Log(); // 可以不限縮型別
log1.run(1); //1
let log2 = new Log<string>(); // 也可以限縮型別
log2.run("2"); //2
要注意的是,型別參數不適用於靜態屬性或方法。類別在今天的文章先暫不探討,等到後面的文章會有較深的探討,先知道通用型別能用在類別中即可。
今天簡單的介紹了通用型別的用法,總體來說,通用型別會用在是指在定義宣告時,不預先指定具體的型別,而是執行時才定義型別,經常使用在函式、介面和類別中,特別是需要接受多種型別參數時,通用型別可提高程式碼的重用性。接下來探討介面(Interface)和類別(Class)還會再更深入的研究通用型別的使用情境,那我們明天見囉!