iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0

泛型在 TypeScript 裡,是很重要的一個環節,幾乎都會看到它的身影。在前面的文章範例裡,應該或多或少都有使用到泛型,那泛型究竟是什麼?為什麼這麼重要呢?泛型可以理解為型別參數,它允許我們在使用函式或類別時可以有一種或多種型別,使其具有更大的彈性,不需要重複為不同的型別創建相似的功能。

泛型函式

泛型函式是一個能夠 處理不同型別的參數和返回值的函式。通過使用泛型,我們可以寫一次函式,然後在不同的地方使用不同的型別,從而實現代碼的重用。

  • 泛型傳統函式寫法
function fn<T>(arg: T): 返回值型別 {
  // ...
}
  • 泛型箭頭函式寫法
const fn = <T>(arg: T): 返回值型別 => {
  // ...
};

注意:T 只是代表一個型別參數,它可以替換成任意文字 ( ex: A、B、C、...... ),不過通常的情形下,潛規則還是以 T 當做泛型的型別名稱來使用。如果有多個泛型的型別名稱,一般則會寫 TUV 等,或是使用當前程式碼更合適的名稱 ( ex: Input、Output、...... )。

我們寫一個簡單的泛型函式,它接受一個參數並返回該參數。

看以下範例:

const create = <T>(arg: T): T => {
  return arg;
};

const createString = create("威爾豬");
console.log(createString); // 輸出: 威爾豬

const createNumber = create(3);
console.log(createNumber); // 輸出: 3

const createBoolean = create(true);
console.log(createBoolean); // 輸出: true

在上面範例中,我們使用 <T> 來指定泛型型別,告訴 TypeScript 我們要在函式中使用一個型別參數 T。當呼叫 create 函式時,我們可以通過傳遞參數的型別來決定返回值的型別。

我們再看另一個範例:

const reverse = <T>(arr: T[]): T[] => {
  return arr.reverse();
};

const numbersArr = [1, 2, 3, 4, 5];
const reversedNumbersArr = reverse(numbersArr);
console.log(reversedNumbersArr); // 輸出: [5, 4, 3, 2, 1]

const stringsArr = ["哈囉", "威爾豬"];
const reversedStringsArr = reverse(stringsArr);
console.log(reversedStringsArr); // 輸出: ["威爾豬", "哈囉"]

在這個範例中,我們創建了一個 reverse 的泛型函式,它接受一個陣列並返回反轉後的陣列。通過使用泛型,便可以依據參數型別知道返回的是什麼型別的陣列。

還記得之前 函式重載 嗎?

我們稍微修改一下範例:

// 函式重載

function userInfo(name: string): void;
function userInfo(name: string, job: string): void;
function userInfo(name: string, job?: string): void {
  const str = job !== undefined ? `${name},現在的職業是${job}` : `${name}`;
  console.log(str);
}

userInfo("威爾豬"); // 輸出: 威爾豬
userInfo("威爾豬", "躺平"); // 輸出: 威爾豬,現在的職業是躺平

那上面函式重載如何使用泛型來改寫呢?

function userInfo<T>(name: T, job?: T): void {
  const str = job !== undefined ? `${name},現在的職業是${job}` : `${name}`;
  console.log(str);
}

userInfo("威爾豬"); // 輸出: 威爾豬
userInfo("威爾豬", "躺平"); // 輸出: 威爾豬,現在的職業是躺平

這樣程式碼就會簡化了許多。

泛型 Promise

泛型在處理 非同步操作 時尤其有用,例如處理 Promise

假設我們有一個函式,用於返回一個 Promise,該 Promise 將返回一個具有特定結果的值,我們可以使用泛型來指定該結果的型別。

看以下範例:

interface IData {
  userId: number;
  id: number;
  title: string;
}

interface ITodo extends IData {
  completed: boolean;
}

interface IPost extends IData {
  body: string;
}

const fetchData = async <T>(url: string): Promise<T> => {
  try {
    const res = await fetch(url);

    if (!res.ok) throw new Error("取得資料失敗!");

    const data = await res.json();

    return data as T;
  } catch (error) {
    throw error;
  }
};

const allData = async () => {
  const promises = [
    fetchData<ITodo>("https://jsonplaceholder.typicode.com/todos/1"),
    fetchData<IPost>("https://jsonplaceholder.typicode.com/posts/1"),
  ];

  try {
    const data = await Promise.all(promises);
    console.log(data);
  } catch (error) {
    console.error((error as Error).message);
  }
};

allData();

在這個例子中,我們創建了一個 fetchData 函式,它使用泛型 <T> 來指定 Promise 的返回結果型別。

泛型與交叉類型

我們可以使用 交叉類型 將不同的型別合併在一起,這同樣適用於泛型。以下是一個使用交叉類型的泛型範例:

const handleMerge = <T, U>(obj1: T, obj2: U): T & U => ({ ...obj1, ...obj2 });

const merge = handleMerge(
  { name: "威爾豬", age: 3 },
  { address: "皇后大道 123 號", job: "躺平" }
);

console.log(merge); //  輸出: {name: "威爾豬", age: 3, address: "皇后大道 123 號", job: "躺平"}

在這個範例中,我們創建了一個 handleMerge 泛型函式,它接受兩個參數並將它們合併成一個交叉類型。通過這樣做,我們可以在返回值中同時訪問兩個物件的屬性。

泛型接口

泛型也可以應用於 接口 interface,可以讓我們創建具有通用型別的接口。

以下是一個泛型接口的範例:

interface IUserData<T, U, V> {
  name: T;
  age: U;
  address: T;
  isStudent: V;
}

const user: IUserData<string, number, boolean> = {
  name: "威爾豬",
  age: 3,
  address: "皇后大道 123 號",
  isStudent: false,
};

console.log(user.name); // 輸出: 威爾豬
console.log(user.age); // 輸出: 3
console.log(user.address); // 輸出: 皇后大道 123 號
console.log(user.isStudent); // 輸出: false

在這個範例中,我們創建了一個名為 IUserData 的泛型接口,它有三個型別參數 TUV。我們使用 <string, number, boolean> 來指定 UserData 接口的型別參數,然後創建了一個符合該接口的 user 物件。

泛型條件約束

有時候,我們可能希望 泛型參數 T 具有特定的限制,以便只適用於某些型別,而不再讓它適用於任何型別,就可以使用泛型約束來實現。

以下是一個泛型約束的範例:

interface ILength {
  length: number;
}

const getLength = <T extends ILength>(arg: T): number => arg.length;

const stringArg = "威爾豬";
const arrayArg = [1, 2, 3, 4, 5];
const objectArg = { name: "威爾豬", length: 10 };

console.log(getLength(stringArg)); // 輸出: 3
console.log(getLength(arrayArg)); // 輸出: 5
console.log(getLength(objectArg)); // 輸出: 10

在這個範例中,我們創建了一個 ILength 接口,它有一個 length 屬性。然後,我們創建了一個 getLength 泛型函式,使用 T extends ILength 來限制泛型參數 T 必須符合 ILength 接口。這樣我們可以確保傳遞給 getLength 函式的參數具有 length 屬性。

我們也可以使用 keyof 讓泛型參數被另一個泛型參數給約束。

例如我們希望傳入函式的其中一個參數一定要是某個物件中的屬性,範例如下:

const getValue = <T, K extends keyof T>(obj: T, key: K): T[K] => obj[key];

const person = { name: "威爾豬", age: 3 };

console.log(getValue(person, "name")); // 輸出: 威爾豬
console.log(getValue(person, "age")); // 輸出: 3
console.log(getValue(person, "address")); // 輸出: undefined ❌ 錯誤,address 不是 person 的屬性

https://ithelp.ithome.com.tw/upload/images/20230921/20141250YWLjKocN01.png

在上面範例中,我們創建了一個名為 getKey 的泛型函式,第一個參數傳入物件 person ,而第二個參數 key 因為是物件 person 屬性的擴展,因此,只能傳入 name 或 age。

泛型類別

泛型不僅可以應用於函式,還可以應用於 類別

以下是一個泛型類別的範例:

class Temp<T> {
  private value: T;
  
  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

const stringTemp = new Temp("威爾豬");
const numberTemp = new Temp(3);

console.log(stringTemp.getValue()); // 輸出: 威爾豬
console.log(numberTemp.getValue()); // 輸出: 3

在這個範例中,我們創建了一個名為 Temp 的泛型類別,它有一個型別參數 T,讓我們可以在實體化時指定不同的型別。

我們再看另一個泛型類別的範例:

class GenericStore<T, U> {
  private data: Map<T, U> = new Map();

  set(key: T, value: U): void {
    this.data.set(key, value);
  }

  get(key: T): U | undefined {
    return this.data.get(key);
  }
  
  keys(): T[] {
    return Array.from(this.data.keys());
  }
}

type unionTypes = string | number | boolean;

const store = new GenericStore<string, unionTypes>();

store.set("name", "威爾豬");
store.set("age", 3);
store.set("isStudent", false);

console.log(store.get("name")); // 輸出: 威爾豬
console.log(store.get("address")); // 輸出: undefined
console.log(store.keys()); // // 輸出: ["name", "age", "isStudent"]

我們創建了一個 GenericStore 的泛型類別,它有型別參數 T、U,分別表示 key 和 value 的型別,這樣 store 就可以儲存不同型別的值。


泛型的主要優勢是允許我們創建通用且可重複使用的程式碼,同時保持型別安全性,透過運用泛型,在處理非同步操作、條件約束等方面也非常有用。我們可以在函式、接口、類別等地方使用泛型,在編譯前捕捉型別錯誤,特別是需要接受多種型別參數時,這使我們能夠操作多種型別而不失去型別信息,以減少運行時的錯誤。


上一篇
非同步處理 Ⅱ (Async / Await)
下一篇
類別 ( Classes )
系列文
用不到 30 天學會基本 TypeScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
Sunny.Cat
iT邦新手 4 級 ‧ 2023-09-21 17:50:37

原來還可以這樣用耶!!
太精彩了~~~

威爾豬 iT邦新手 3 級 ‧ 2023-09-21 21:54:34 檢舉

哇嗚~感謝支持!!
希望對妳的書單系統有一丁丁丁點的小幫助,期待好文分享~~~

我要留言

立即登入留言