本文 AI 產出,尚未審核
TypeScript 泛型型別別名(Generic Type Alias)
簡介
在大型 TypeScript 專案中,型別的復用與可讀性往往是維護成本的關鍵。
傳統上,我們會直接在介面(interface)或類別(class)上使用泛型,然而當型別的結構變得複雜、需要在多個地方重複使用時,型別別名(type alias) 變成了更靈活的選擇。
結合泛型,泛型型別別名 能讓我們以極簡的語法定義高度抽象、可參數化的型別,從而在整個程式碼基礎上提升型別安全與開發效率。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,循序漸進地帶你掌握泛型型別別名的使用方式,適合 初學者到中級開發者 參考。
核心概念
1️⃣ 為什麼要使用型別別名?
- 可讀性:一次定義,多處引用,讓程式碼更易於理解。
- 抽象化:把重複的型別結構抽離,讓 API 設計更具彈性。
- 組合性:型別別名可以與 條件型別、映射型別 等進階功能結合,產生更強大的型別工具。
註:
type只能定義型別,無法被實例化;若需要類別行為,仍須使用class或interface。
2️⃣ 基本語法
// 定義一個簡單的泛型型別別名
type Box<T> = {
value: T;
};
// 使用方式
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "Hello" };
T為型別參數(type parameter),在使用別名時必須提供具體型別。- 若不提供,TypeScript 會嘗試推斷(type inference),但在某些情況下必須顯式指定。
3️⃣ 加上型別約束(Constraint)
// 只接受具備 length 屬性的型別
type WithLength<T extends { length: number }> = {
data: T;
length: T["length"];
};
const arrBox: WithLength<number[]> = { data: [1, 2, 3], length: 3 };
const strBox: WithLength<string> = { data: "TS", length: 2 };
T extends { length: number }為型別限制,保證傳入的型別一定有length屬性。- 這樣可以在型別別名內安全存取
T["length"],避免編譯錯誤。
4️⃣ 預設型別參數(Default Type Parameter)
type Response<T = any> = {
status: number;
payload: T;
};
// 若不指定 T,預設為 any
const defaultRes: Response = { status: 200, payload: "任意資料" };
const userRes: Response<{ id: number; name: string }> = {
status: 200,
payload: { id: 1, name: "Alice" },
};
- 預設參數讓別名在快速開發時更便利,同時仍保有可自訂的彈性。
5️⃣ 與條件型別結合:建立「可選」屬性
type OptionalIf<T, K extends keyof T> = K extends keyof T
? { [P in K]?: T[P] } & Omit<T, K>
: T;
// 範例:將 Person 的 `age` 變成可選
type Person = { name: string; age: number; email: string };
type PersonWithOptionalAge = OptionalIf<Person, "age">;
// 使用
const p1: PersonWithOptionalAge = { name: "Bob", email: "bob@example.com" };
const p2: PersonWithOptionalAge = { name: "Carol", age: 30, email: "c@example.com" };
- 透過 條件型別 + 映射型別,我們可以在同一個別名中動態改變屬性的必填/可選狀態,極大提升 API 的彈性。
6️⃣ 泛型函式型別別名
type Mapper<T, U> = (input: T) => U;
// 具體實作
const numberToString: Mapper<number, string> = (n) => n.toString();
const userToId: Mapper<{ id: number; name: string }, number> = (u) => u.id;
- 把函式簽名抽成別名後,重複使用變得非常簡潔,且在大型代碼庫中易於統一管理。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 分配式條件型別(Distributive Conditional Types) 造成意外結果 | 當條件型別的左側是聯合型別時,會被自動分配到每個成員上。 | 若不希望分配,可使用 T extends any ? ... : ... 包裹,或加上 never 之類的技巧。 |
| 型別推斷失敗 | 在某些複雜的映射型別裡,TS 無法正確推斷 T,導致 any 回傳。 |
明確指定型別參數或使用 as const 斷言。 |
| 循環引用 | 型別別名相互引用會形成無窮遞迴,編譯器會報錯 Type alias 'X' circularly references itself。 |
把其中一方改成 介面(interface)或使用 遞迴條件型別(帶上限制)。 |
| 過度抽象 | 把所有型別都寫成別名,會讓閱讀者失去上下文,降低可維護性。 | 只在重複度高或語意明確的情況下抽離;保持適度的具體描述。 |
最佳實踐
- 命名要具體:
Result<T>、Paginated<T>等名稱能直接傳達意圖。 - 盡量使用預設參數:減少呼叫端的冗長,同時保留可自訂性。
- 結合
readonly:若資料不應被變更,使用Readonly<T>或在別名內加上readonly。 - 文件化:在型別別名上方加上 JSDoc,讓 IDE 能提供完整提示。
- 測試型別:利用
// @ts-expect-error或type Assert<T extends true> = T;在測試檔中驗證型別行為。
實際應用場景
1️⃣ API 回傳資料的通用型別
type ApiResponse<T = any> = {
/** HTTP 狀態碼 */
status: number;
/** 成功與否 */
ok: boolean;
/** 具體資料 */
data: T;
/** 錯誤訊息(若有) */
error?: string;
};
// 例:取得使用者列表
type User = { id: number; name: string };
type UsersRes = ApiResponse<User[]>;
// 使用
async function fetchUsers(): Promise<UsersRes> {
const res = await fetch("/api/users");
const json = await res.json();
return { status: res.status, ok: res.ok, data: json };
}
- 只要改變
T,同一套回傳型別即可適用所有端點,減少重複且 保持一致。
2️⃣ Redux/Pinia 狀態管理的型別抽象
type EntityState<T> = {
byId: Record<string, T>;
allIds: string[];
loading: boolean;
error?: string;
};
type Todo = { id: string; title: string; completed: boolean };
type TodoState = EntityState<Todo>;
- 透過
EntityState<T>,不同的資料模型(Todo、Post、User)只需要提供 單一型別參數,即可得到完整的狀態結構。
3️⃣ 建立「可選」屬性工具型別
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 讓 User 的 email 變成可選
type User = { id: number; name: string; email: string };
type UpdateUser = PartialBy<User, "email">;
- 在表單更新、Patch API 等情境下,只允許部分欄位被傳入,型別別名提供了乾淨的解法。
4️⃣ 通用資料轉換函式
type Mapper<T, U> = (src: T) => U;
function mapArray<T, U>(arr: T[], mapper: Mapper<T, U>): U[] {
return arr.map(mapper);
}
// 範例:把 User 陣列轉成姓名陣列
type User = { id: number; name: string };
const names = mapArray<User, string>(users, (u) => u.name);
- 把 映射邏輯 抽成
Mapper,不只提升可讀性,也讓函式在不同型別間保持 通用。
總結
- 泛型型別別名 是 TypeScript 中極具威力的抽象工具,能把複雜的型別結構濃縮為簡潔、可重用的宣告。
- 透過 型別參數、約束、預設值,我們可以在保證型別安全的前提下,寫出高度彈性的 API、狀態管理與工具函式。
- 使用時需留意 分配式條件型別、推斷失敗 與 循環引用 等常見陷阱,並遵循 具體命名、適度抽象、文件化 的最佳實踐。
- 在實務專案中,從 API 回傳、Redux 狀態、可選屬性工具 到 資料映射函式,泛型型別別名都有豐富的應用場景,能顯著提升程式碼的可維護性與可讀性。
掌握了這些概念與技巧後,你將能在 TypeScript 專案中更自信地設計型別,讓開發團隊在 型別安全 與 開發效率 之間取得最佳平衡。祝開發順利! 🚀