TypeScript 系列第三篇;建議先讀基礎篇與泛型篇(同目錄
basics.md、generics.md)。
目錄
- 型別即運算
- keyof、typeof、索引存取
- 映射型別(Mapped Types)
- 條件型別(Conditional Types)
- infer:在條件型別中推斷
- 模板字面值型別
- 內建工具型別(Utility Types)
- 實戰範例
- 常見問題
- 總結
型別即運算
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:型別 → 屬性名 unionT[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 }
內建的
Partial、Readonly、Pick、Record全都是 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[](逐個分配)
這是 Exclude、Extract、NonNullable 等工具型別的運作原理。
// 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"
內建字串操作型別:Uppercase、Lowercase、Capitalize、Uncapitalize。
內建工具型別(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> |
排除 null 與 undefined |
函式 / 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 有關係嗎?
有。內建的 Partial、Readonly、Pick、Record 等就是用 mapped types 實作的;Exclude、ReturnType 等則用 conditional types + infer。utility types 是「官方寫好的型別運算」。
問題 2:Pick 和 Omit 怎麼選?
要保留的屬性少 → 用 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