【TypeScript 小教室 - 能讓物件安全操作多種靜態型別的神奇機制】
泛用型別 (Generic type) ──簡稱泛型──讓使用者在定義函式或類別的時候先不預先決定好具體的型別, 等到它們被呼叫的時候再視傳入的資料而定。這使得函式或類別得以應付多種資料型別, 卻又能確保資料操作的安全性。
若要理解泛型的使用方式與其便利之處, 最佳方式就是看個實際範例, 這能展示正規型別為何在某些時候會變得難以管理。
在下面的程式中, 我們從一個 datatype 模組匯入 Person 及 Product 類別, 並分別建立兩個物件、用一個陣列來走訪。現在我們想定義一個新類別 DataCollection 來協助我們管理 Person 型別物件:
import { Person, Product } from "./dataTypes.js";
// 包含 Person 物件的陣列
let people = [
new Person("Bob Smith", "London"),
new Person("Dora Peters", "New York")
];
// 包含 Product 物件的陣列
let products = [
new Product("Running Shoes", 100),
new Product("Hat", 25)
];
// 只能操作 Person 物件陣列的 DataCollection 類別
class DataCollection {
private items: Person[] = [];
constructor(initialItems: Person[]) {
this.items.push(...initialItems);
}
getNames(): string[] {
return this.items.map(item => item.name);
}
getItem(index: number): Person {
return this.items[index];
}
}
// 建立 DataCollection 物件並將 people 放入其屬性
let data = new DataCollection(people);
// 印出人名陣列, 使用 join() 將之連成字串並以逗號分隔
console.log(Names: ${data.getNames().join(", ")});
// 取得 people 陣列的第一筆並印出其內容
let firstData = data.getItem(0);
console.log(First data: ${firstData.name}, ${firstData.city});
現在 Person 物件集合會以私有屬性的型式儲存在 PeopleCollection 類別的物件中 ( 透過建構子加入), 並提供我們兩個查詢介面:getNames() 方法會傳回一個陣列, 裡面包含每個 Person 物件的 name 屬性的值, 而getItem() 方法則允許 Person 物件透過索引來取值。
以上 DataCollection 類別的問題在於, 它『只能』拿來管理 Person 型別的物件。若我也想對 Product 物件套用同樣的操作, 顯然就得做出一些妥協。我當然可以複製貼上一個新類別, 但若未來還得支援其他的型別, 重複的程式碼很快就會膨脹到難以管理的地步。
所謂泛型類別, 就是一個類別在定義時以泛型參數 (generic type parameter) 取代明確型別。泛型參數是一個暫時頂替用的型別, 等到類別被用來建立新物件時再來明確指定之。這使得就算我們事先不知道類別要使用哪種型別, 也可以在類別中針對某種特定型別進行操作。請看以下的示範:
// 使用泛型 T 的 DataCollection 類別
class DataCollection<T> {
private items: T[] = []; // 用 T 來註記型別
constructor(initialItems: T[]) {
this.items.push(...initialItems);
}
getNames(): string[] {
return this.items.map(item => { // 用型別防衛敘述檢查 T 是否為 Person 或 Product 型別
if (item instanceof Person || item instanceof Product) {
return item.name;
} else {
return null;
}
});
}
getItem(index: number): T {
return this.items[index];
}
}
這樣的宣告結果就是一個泛型類別 DataCollection, 它擁有一個泛型型別 T, 這型別屆時會被某個特定的型別取代。然後我們就能在類別內的任何地方把 T 當成實際型別使用。舉個例, 我們可以像下面這樣讓建構子接受一個型別是 Person 或 Product 的陣列引數:
let data = new DataCollection<Person>(people); // 泛型參數為 Person 型別
let data2 = new DataCollection<Product>(products); // 泛型參數為 Product 型別
現在我們會建立兩個物件, 一個是 DataCollection<Person> 型別, 另一個是 DataCollection<Product> 型別, 並於其建構子傳入不同型別的物件集合。TypeScript 會掌握 data 與 data2 所使用的泛型型別, 並確保只有指定的型別能用在對應類別中。
--- 以上內容節錄並修改自旗標出版《TypeScript 邁向專家之路》
* * *
泛型程式設計是比較進階的主題,但在過去已經出現在像是 C++ 或 Java 等語言,如今像是 C#、Rust 和未來的 Go 語言也都支援泛型。
我們這本書討論到的 TypeScript 泛型主題不少,例如:
‧如何限制上述的 T 型別只能指定特定幾個型別?
‧如何將泛型套用到介面 (interface) 或 Set、Map 容器物件?
‧如何用泛型來做型別映射 (type mapping),也就是從既有的物件型別產生類似的新型別?
‧如何在建立型別時,用「條件型別」動態決定其實際型別?
在一般情況下,泛型的用意是讓你用更少的程式碼做更多事。不過,許多前端框架在搭配 TypeScript 時,也會運用泛型來註記像是元件的屬性、狀態值型別。舉個例,第 19 章中我們會看到 React 用 FunctionComponent 來定義元件函式時,就可以使用泛型參數:
import { ChangeEvent, FunctionComponent, useState } from "react";
import { Product } from "./data/entities";
// 元件屬性 (props) 的型別
interface Props {
product: Product,
callback: (product: Product, quantity: number) => void
}
// 定義元件來顯示網頁上的一筆產品
export const ProductItem: FunctionComponent<Props> = (props) => {
// 用泛型定義狀態值 (state)
const [quantity, setQuantity] = useState<number>(1);
// 對 props.product, product.callback 與 quantity 進行操作 ...
}
ProductItem 函式接收一個參數 props, 其型別由介面 Props 定義, 而 props 內的屬性值會由 ProductItem 的上層元件傳入。既然這裡使用了泛型參數,假如上層元件嘗試傳入錯誤型別的值,編譯時就會報錯 (或者在 VS Code 之類的編輯器中會直接提示錯誤)。
由此可見,泛型參數使得你能定義各式各樣的 React 元件、操作不同型別的值,同時確保開發人員在跨元件傳遞資料時不至於弄錯型別、減少除錯的時間。
[本文內相關書籍參考]
-想學網頁技術?這次就先把 TypeScript 學好吧
[相關通路優惠]
天瓏合購75折
https://pse.is/3rkvgk
博客來2書合購75折
https://pse.is/3rr8a7