【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 學好吧![《TypeScript 邁向專家之路》](https://www.books.com.tw/products/0010906604) [相關通路優惠] 天瓏合購75折 https://pse.is/3rkvgk 博客來2書合購75折 https://pse.is/3rr8a7