TypeScript 進階型別與工具型別完全指南

深入 TypeScript 型別運算:mapped types、conditional types、infer、template literal types 與內建 utility types

TypeScript 系列第三篇;建議先讀基礎篇與泛型篇(同目錄 basics.mdgenerics.md)。


目錄


型別即運算

TypeScript 的型別系統本身是一套型別層級的運算語言:可以從既有型別推導、轉換、組合出新型別,而不必手動重寫。本篇的 mapped / conditional / infer / template literal 就是這些「型別運算」的工具,內建的 utility types 也都是用它們組出來的。

既有型別 ──(型別運算)──► 衍生型別
User ──去掉密碼欄位──► PublicUser
User ──全部變選填──► UserPatch

keyof、typeof、索引存取

進階型別的三個基礎積木:

type User = { id: number; name: string; active: boolean };

// keyof:取所有屬性名的 union
type UserKeys = keyof User;        // "id" | "name" | "active"

// 索引存取:取某屬性的型別
type IdType = User["id"];          // number
type Vals = User[keyof User];      // number | string | boolean

// typeof:從「值」反推「型別」
const config = { host: "localhost", port: 5432 };
type Config = typeof config;       // { host: string; port: number }
  • keyof T:型別 → 屬性名 union
  • T[K]:索引存取,取屬性型別
  • typeof value:值 → 型別(在型別位置使用)

映射型別(Mapped Types)

Mapped Types 遍歷一個型別的所有屬性,產生新型別——「對每個屬性做某件事」。

type User = { id: number; name: string };

// 把每個屬性都變成唯讀
type ReadonlyUser = { readonly [K in keyof User]: User[K] };
// = { readonly id: number; readonly name: string }

// 把每個屬性都變成選填
type PartialUser = { [K in keyof User]?: User[K] };
// = { id?: number; name?: string }

修飾子(Modifiers)

可加 / 移除 readonly?,用 + / -

// 移除 readonly(- 表移除)
type Mutable<T> = { -readonly [K in keyof T]: T[K] };

// 移除選填(變必填)
type Required2<T> = { [K in keyof T]-?: T[K] };

重新映射 key(as)

// 給每個 getter 產生 getXxx 方法名
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Getters<{name: string}> = { getName: () => string }

內建的 PartialReadonlyPickRecord 全都是 mapped types。


條件型別(Conditional Types)

Conditional Types 是型別層級的三元運算:T extends U ? X : Y

type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;    // true
type B = IsString<number>;  // false

分配律(Distributive)

條件型別作用在 union 上時會逐成員分配

type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;   // string[] | number[](逐個分配)

這是 ExcludeExtractNonNullable 等工具型別的運作原理。

// Exclude 的本質
type MyExclude<T, U> = T extends U ? never : T;
type C = MyExclude<"a" | "b" | "c", "b">;   // "a" | "c"

infer:在條件型別中推斷

infer 讓你在條件型別裡抓出某個位置的型別,宣告一個臨時型別變數。

// 取出陣列的元素型別
type ElementType<T> = T extends (infer E)[] ? E : T;
type E1 = ElementType<string[]>;   // string
type E2 = ElementType<number>;     // number(不是陣列,回自己)

// 取出函式的回傳型別(這就是內建 ReturnType 的原理)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => number>;   // number

// 取出 Promise 解析的型別
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type U1 = Unwrap<Promise<string>>;   // string

infer R 的意思:「我不知道這裡是什麼型別,幫我推斷出來並叫它 R」。是 ReturnType / Parameters / Awaited 等工具型別的核心。


模板字面值型別

字串層級的型別運算,能用字面值型別拼出新字串型別:

type Lang = "zh" | "en";
type Page = "home" | "about";

type Route = `/${Lang}/${Page}`;
// "/zh/home" | "/zh/about" | "/en/home" | "/en/about"(自動展開所有組合)

// 搭配內建字串工具型別
type Event = "click" | "hover";
type Handler = `on${Capitalize<Event>}`;   // "onClick" | "onHover"

內建字串操作型別:UppercaseLowercaseCapitalizeUncapitalize


內建工具型別(Utility Types)

TypeScript 內建一批常用的型別轉換工具,全都用前面的機制實作:

物件轉換

工具型別 作用 範例
Partial<T> 全部屬性變選填 Partial<User>
Required<T> 全部屬性變必填 Required<User>
Readonly<T> 全部屬性變唯讀 Readonly<User>
Pick<T, K> 只挑選某些屬性 Pick<User, "id" | "name">
Omit<T, K> 排除某些屬性 Omit<User, "password">
Record<K, V> 建立 key→value 的物件型別 Record<string, number>

Union 篩選

工具型別 作用
Exclude<T, U> 從 T 排除可指派給 U 的成員
Extract<T, U> 從 T 取出可指派給 U 的成員
NonNullable<T> 排除 nullundefined

函式 / Promise

工具型別 作用
ReturnType<F> 取函式回傳型別
Parameters<F> 取函式參數型別(tuple)
Awaited<T> 取 Promise 解析後的型別

範例

interface User {
  id: number;
  name: string;
  password: string;
}

type PublicUser = Omit<User, "password">;        // 去掉密碼
type UserPatch = Partial<Pick<User, "name">>;    // { name?: string }
type UserMap = Record<number, User>;             // { [id: number]: User }

function getUser() { return { id: 1, name: "A" }; }
type U = ReturnType<typeof getUser>;             // { id: number; name: string }

Omit + Pick + Partial 組合是日常最常用的——從一個資料模型衍生出 DTO、表單、補丁型別,不必重寫。


實戰範例

1. 從資料模型衍生多種型別

interface Product {
  id: number;
  name: string;
  price: number;
  createdAt: Date;
}

type ProductCreateInput = Omit<Product, "id" | "createdAt">;  // 建立時不需 id/時間
type ProductUpdateInput = Partial<ProductCreateInput>;        // 更新時全選填
type ProductSummary = Pick<Product, "id" | "name">;           // 列表只要 id/name

2. 深度唯讀(遞迴 mapped + conditional)

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

3. 取出物件的值型別

const roles = { admin: 1, user: 2, guest: 3 } as const;
type RoleValue = (typeof roles)[keyof typeof roles];   // 1 | 2 | 3

常見問題

問題 1:mapped type 和 utility type 有關係嗎?

有。內建的 PartialReadonlyPickRecord 等就是用 mapped types 實作的;ExcludeReturnType 等則用 conditional types + infer。utility types 是「官方寫好的型別運算」。

問題 2:PickOmit 怎麼選?

要保留的屬性少 → 用 Pick(列出要的);要排除的屬性少 → 用 Omit(列出不要的)。兩者互補。

問題 3:infer 是做什麼的?

在條件型別裡「抓出」某個位置的型別並命名。例如從函式型別抓回傳值(ReturnType)、從陣列抓元素、從 Promise 抓解析型別。

問題 4:條件型別為什麼會「分配」?

當條件型別作用在裸的型別參數且該參數是 union 時,TS 會對 union 每個成員分別套用再合併。這是 Exclude/Extract 能篩選 union 的原理。要關閉分配可用 [T] extends [U]

問題 5:這些進階型別實務上常用嗎?

utility types(Partial/Pick/Omit/Record/ReturnType…)極常用,幾乎天天碰;自己手寫 mapped/conditional/infer 較少,但讀懂它們才能理解 utility types 與函式庫型別。


總結

核心要點

  • TS 型別系統是一套型別運算語言,能從既有型別推導出新型別
  • 基礎積木:keyof(屬性名)、T[K](索引存取)、typeof(值→型別)
  • Mapped types:遍歷屬性產生新型別(可加減 readonly / ?、用 as 重映射 key)
  • Conditional types T extends U ? X : Y:型別三元運算,對 union 分配
  • infer:在條件型別中抓出並命名某位置的型別
  • 模板字面值型別:字串層級的型別組合
  • 內建 utility types 是上述機制的成品,Partial/Pick/Omit/Record/ReturnType 最常用

快速參考

機制 用途
keyof / T[K] / typeof 取屬性名 / 屬性型別 / 值的型別
{ [K in keyof T]: ... } mapped type
T extends U ? X : Y conditional type
infer R 在條件型別中推斷型別
`${A}${B}` 模板字面值型別
Partial/Pick/Omit/Record 最常用的物件轉換
ReturnType/Parameters/Awaited 函式 / Promise 型別擷取

建立日期:2026-06-18

🔗相關文章