泛型在 TypeScript 裡,是很重要的一個環節,幾乎都會看到它的身影。在前面的文章範例裡,應該或多或少都有使用到泛型,那泛型究竟是什麼?為什麼這麼重要呢?泛型可以理解為型別參數
,它允許我們在使用函式或類別時可以有一種或多種型別,使其具有更大的彈性,不需要重複為不同的型別創建相似的功能。
泛型函式是一個能夠 處理不同型別的參數和返回值的函式
。通過使用泛型,我們可以寫一次函式,然後在不同的地方使用不同的型別,從而實現代碼的重用。
function fn<T>(arg: T): 返回值型別 {
// ...
}
const fn = <T>(arg: T): 返回值型別 => {
// ...
};
注意:
T 只是代表一個型別參數
,它可以替換成任意文字 ( ex: A、B、C、...... ),不過通常的情形下,潛規則還是以T
當做泛型的型別名稱來使用。如果有多個泛型的型別名稱,一般則會寫T
、U
、V
等,或是使用當前程式碼更合適的名稱 ( 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 將返回一個具有特定結果的值,我們可以使用泛型來指定該結果的型別。
看以下範例:
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 的泛型接口,它有三個型別參數 T
、U
和 V
。我們使用 <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 的屬性
在上面範例中,我們創建了一個名為 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 就可以儲存不同型別的值。
泛型的主要優勢是允許我們創建通用且可重複使用的程式碼,同時保持型別安全性,透過運用泛型,在處理非同步操作、條件約束等方面也非常有用。我們可以在函式、接口、類別等地方使用泛型,在編譯前捕捉型別錯誤,特別是需要接受多種型別參數時,這使我們能夠操作多種型別而不失去型別信息,以減少運行時的錯誤。
原來還可以這樣用耶!!
太精彩了~~~
哇嗚~感謝支持!!
希望對妳的書單系統有一丁丁丁點的小幫助,期待好文分享~~~