iT邦幫忙

2024 iThome 鐵人賽

DAY 24
1
JavaScript

TypeScript 初學者也能看的學習指南系列 第 24

TypeScript 初學者也能看的學習指南 24 - Generics 泛型 X 泛型約束

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241004/20149362Bj0h99jcKM.png

繼昨天的泛型函式,今天要來介紹「泛型約束」

大綱

  • 泛型約束
  • 範例 - 泛型約束
  • keyof 關鍵字
  • 當 Interface 遇上 Generics
  • 當 Class 遇上 Generics
  • 牛刀小試

泛型約束 Generics Constraints

我們知道泛型中的 T ,代表一個型別參數(Type Parameter),它可代進任意輸入的型別
我們可以使用 extends 關鍵字來定義「泛型約束」。告訴 TypeScript ,泛型的「型別參數」必須符合特定的型別結構
https://ithelp.ithome.com.tw/upload/images/20241004/20149362zKzYuwGjPj.png

範例1

interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

loggingIdentity(3);                        // ❌ Error
loggingIdentity({ length: 10, value: 3 }); // ✅ Pass

loggingIdentity 函式中的泛型約束 <Type extends Lengthwise> 約束了任何傳入的 arg 都必須有一個 length 屬性

loggingIdentity(3) 因為不符合參數必須有 length 這個屬性的規定,所以報錯,錯誤訊息如下
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'

附上圖解
https://ithelp.ithome.com.tw/upload/images/20241004/20149362wofvO63WJd.png

範例2

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { 
  a: 1, 
  b: 2, 
  c: 3, 
  d: 4 
};
 
getProperty(x, "a"); // ✅ Pass
getProperty(x, "m"); // ❌ Error

這個函式中有兩個型別參數,分別是 Type Key
Type 可以接受任何類型的值
Key 受到 Type 的鍵( = 物件的屬性名)的約束。這也代表 Key 必須是 Type 中存在的鍵,也就是 a, b, c, d,這個約束防止函式被傳入任何不存在的鍵
這也是為何 getProperty(x, 'm') 會報錯,因為 x 裡沒有 m 這個屬性

keyof 關鍵字

keyof 是 TypeScript 的操作符,用來取得「物件型別」的所有「鍵」的聯合(union)
這是 TypeScript 中處理物件屬性時很好用的操作符,確保在編譯時,對物件屬性的引用就已是安全的

interface Person {
    name: string;
    age: number;
}

type PersonKeys = keyof Person; // 'name' | 'age'

滑鼠移到上面這段會出現下方的推斷
https://ithelp.ithome.com.tw/upload/images/20241004/20149362qPaIYYZ81u.png

宣告一個變數指向 PersonKeys,值只能是 name 或 age
https://ithelp.ithome.com.tw/upload/images/20241004/20149362mCvaiDdJOT.png


雖然陣列和元組等也屬於「物件型別」的一種,但在 keyof 的情境中,「物件型別」通常是指有具名屬性的物件結構,常見於介面(Interface)類別(Class)型別別名(Type Alias)

但這不代表 TypeScript 禁止 keyof 用在陣列和元組等其他物件型別,只是這樣做沒什麼意義(等等看下面範例就會知道了😆),TypeScript 對它們的處理方式與一般物件型別略有不同

  • 陣列的 keyof
let numbers: number[] = [1, 2, 3];
type ArrayKeys = keyof typeof numbers;  // type ArrayKeys = number | "length" | "push" | "pop" | ...

當你在陣列中使用 keyof 時,推斷出的是陣列操作方法名稱的 union
https://ithelp.ithome.com.tw/upload/images/20241004/20149362B3awzJR8Zg.png


  • 元組的 keyof
let tuple: [string, number] = ["hello", 42];
type TupleKeys = keyof typeof tuple;  // type TupleKeys = "0" | "1" | "length" | "push" | "pop" | ...

元組的 keyof 會回傳 index 和陣列操作方法的名稱
https://ithelp.ithome.com.tw/upload/images/20241004/20149362BJc49Dh5ST.png

這樣做其實沒什麼意義,對吧
只是單純抱有實驗精神把實驗結果記錄一下😂

當 Interface 遇上 Generics

泛型介面(Generic Interface)的定義方式類似於泛型函式。你可以指定一個或多個型別參數,這些參數可以用於介面中的屬性、方法

interface Container<T> {
    value: T;
    add(value: T): void;
}

當 Class 遇上 Generics

泛型類別(Generic Classes)會在 Class Name 後會加上型別參數,表示該 Class 是泛用的

class Box<T> {
    private contents: T;

    constructor(value: T) {
        this.contents = value;
    }

    get(): T {
        return this.contents;
    }

    set(value: T): void {
        this.contents = value;
    }
}

let stringBox = new Box<string>("hello");  // hello
let numberBox = new Box<number>(123);      // 123

牛刀小試

開發一個函式 filterItems,該函式接受一個陣列物件格式,並回傳所有具有特定屬性的物件。使用「泛型約束」來確保每個物件都有包含該屬性

interface WithID {
    id: string;
}
// 👇 調整為泛型函式,並使用 WithID 做為泛型約束的條件
function filterItems() {
    return items.filter(item => item.id.startsWith("#"));
}

// ✅ Pass
const filteredItems = filterItems([{ id: '#a' }, { id: '123' }]);

// ❌ Error 因為陣列中的物件沒有 id 屬性
// const errorFilteredItems = filterItems([{ name: 1 }, { name: 2 }]);

每天的內容有推到 github 上喔

References


上一篇
TypeScript 初學者也能看的學習指南 23 - Generics 泛型 X 泛型函式
下一篇
TypeScript 初學者也能看的學習指南 25 - Generics 泛型 X 參數預設值 X type 應用
系列文
TypeScript 初學者也能看的學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言