本文 AI 產出,尚未審核

TypeScript 泛型型別別名(Generic Type Alias)

簡介

在大型 TypeScript 專案中,型別的復用與可讀性往往是維護成本的關鍵。
傳統上,我們會直接在介面(interface)或類別(class)上使用泛型,然而當型別的結構變得複雜、需要在多個地方重複使用時,型別別名(type alias) 變成了更靈活的選擇。
結合泛型,泛型型別別名 能讓我們以極簡的語法定義高度抽象、可參數化的型別,從而在整個程式碼基礎上提升型別安全開發效率

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,循序漸進地帶你掌握泛型型別別名的使用方式,適合 初學者到中級開發者 參考。


核心概念

1️⃣ 為什麼要使用型別別名?

  • 可讀性:一次定義,多處引用,讓程式碼更易於理解。
  • 抽象化:把重複的型別結構抽離,讓 API 設計更具彈性。
  • 組合性:型別別名可以與 條件型別映射型別 等進階功能結合,產生更強大的型別工具。

type 只能定義型別,無法被實例化;若需要類別行為,仍須使用 classinterface


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)或使用 遞迴條件型別(帶上限制)。
過度抽象 把所有型別都寫成別名,會讓閱讀者失去上下文,降低可維護性。 只在重複度高語意明確的情況下抽離;保持適度的具體描述。

最佳實踐

  1. 命名要具體Result<T>Paginated<T> 等名稱能直接傳達意圖。
  2. 盡量使用預設參數:減少呼叫端的冗長,同時保留可自訂性。
  3. 結合 readonly:若資料不應被變更,使用 Readonly<T> 或在別名內加上 readonly
  4. 文件化:在型別別名上方加上 JSDoc,讓 IDE 能提供完整提示。
  5. 測試型別:利用 // @ts-expect-errortype 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 專案中更自信地設計型別,讓開發團隊在 型別安全開發效率 之間取得最佳平衡。祝開發順利! 🚀