iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
2
自我挑戰組

Typescript 初心者手札系列 第 17

【Day 17】TypeScript 資料型別 - 通用型別(Generic Types)

閱讀今天的文章前,先回顧一下昨天的學習,回答看看:

  • any 和 unknown 型別的差別為何?
  • 倘若希望 TypeScript 編譯時禁止隱性推論 any 型別,要如何做設定呢?

如果有點不清楚答案的話,可以看看 Day16 的文章喔!

通用型別(Generics Types)用途

通用型別(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,且傳入型別可以是任何型別。

型別變數(Type variables)

型別變數 T ,又稱為型別參數(type parameters)和通用參數(generic parameters),有下面幾個特性:

  • Type variable, a special kind of variable that works on types rather than values.(型別變數代表一個特殊型別,一般變數代表一個值)
  • This allows us to traffic that type information in one side of the function and out the other.(型別變數傳遞型別資訊,而一般變數傳遞值)
  • T 只是個型別代詞,換成其他字母如 A 、B 等都可以。

通用函式

來試著創建一個通用函式並執行,執行函式時可以只傳函式參數(型別參數 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;
};

通用介面(Generics Interface)

除了函式會常使用通用型別外,介面(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 。

通用類別(Generics Types)

和通用介面類似,通用型別也能用在類別(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)還會再更深入的研究通用型別的使用情境,那我們明天見囉!


上一篇
【Day 16】TypeScript 資料型別 - 特殊型別(下)- Any & Unknown
下一篇
【Day 18】TypeScript 資料型別 - 介面(Interface)宣告與屬性
系列文
Typescript 初心者手札30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言