上一篇講的泛型、keyof、infer、mapped、conditional 都是「素材」,這一篇看標準庫用這些素材組好的成品:Partial、Pick、Omit、Record、ReturnType⋯⋯。會用是日常戰力;理解背後怎麼組的,遇到內建不夠用時才有辦法自己擴充。所以下面每個工具都會附上它的實作(或等價寫法),對照前一篇的機制看。
Partial<T> / Required<T>
一個把所有屬性變可選,一個變必填:
type Partial<T> = { [K in keyof T]?: T[K] }
type Required<T> = { [K in keyof T]-?: T[K] }
interface User {
id: number
name: string
email?: string
}
type UserPatch = Partial<User>
// { id?: number; name?: string; email?: string }
type FullUser = Required<User>
// { id: number; name: string; email: string }
最常見的用途是 update:用 Partial<User> 接收任意子集的欄位。
Readonly<T>
所有欄位加上 readonly:
type Readonly<T> = { readonly [K in keyof T]: T[K] }
const u: Readonly<User> = { id: 1, name: 'a' }
u.id = 2 // ❌
注意它是 shallow readonly,只鎖最外層,深層巢狀屬性還是可變。要深 readonly 得自己遞迴:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}
兩者的差別:
type Config = { db: { host: string } }
const c1: Readonly<Config> = { db: { host: 'x' } }
c1.db = { host: 'y' } // Error: db is readonly
c1.db.host = 'y' // OK(只鎖最外層)
const c2: DeepReadonly<Config> = { db: { host: 'x' } }
c2.db.host = 'y' // Error: host is readonly(深層也鎖)
Pick<T, K>:挑選欄位
從 T 挑出指定的幾個欄位組成新型別:
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }
Omit<T, K>:排除欄位
反過來,把指定欄位剔掉、留下其餘的:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string }
注意 K 沒有限定 keyof T,所以傳一個不存在的 key 進去 TS 也不會抱怨——這是官方刻意的設計取捨。
Record<K, V>:等值物件
所有 key 都對應同一種 value 型別:
type Record<K extends keyof any, V> = { [P in K]: V }
type Scores = Record<'math' | 'english' | 'science', number>
// { math: number; english: number; science: number }
type Cache = Record<string, unknown>
// 任意字串 key 對應任意值
比 index signature 的 { [k: string]: V } 更靈活,因為 K 可以是字面值 union,把 key 限定在固定幾個。
Exclude<T, U> / Extract<T, U>
這兩個是對 union 做篩選,實作就是上一篇講的 distributive conditional:
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never
type T1 = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
type T2 = Extract<string | number | boolean, string | number> // string | number
前面 Omit<T, K> 的實作裡就是靠 Exclude 從 keyof T 剔掉 K。
NonNullable<T>
清掉 union 裡的 null 與 undefined:
type NonNullable<T> = T & {} // TS 4.8+ 改用這個寫法
type X = NonNullable<string | null | undefined> // string
ReturnType<T> / Parameters<T>
用 infer 從函式型別抽出回傳值與參數:
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
type Parameters<T> = T extends (...args: infer P) => any ? P : never
function fetchUser(id: number, cache: boolean) {
return { id, name: 'a' }
}
type R = ReturnType<typeof fetchUser> // { id: number; name: string }
type P = Parameters<typeof fetchUser> // [id: number, cache: boolean]
實務上多半配 typeof someFunction 用,函式簽章改了型別自動跟上,不用手動同步。
ConstructorParameters<T> / InstanceType<T>
class 版本的對應工具——抽 constructor 參數、抽實例型別:
class Greeter {
constructor(public name: string, public age: number) {}
}
type CP = ConstructorParameters<typeof Greeter> // [name: string, age: number]
type I = InstanceType<typeof Greeter> // Greeter
Awaited<T>:拆 Promise
type A1 = Awaited<Promise<string>> // string
type A2 = Awaited<Promise<Promise<number>>> // number(會遞迴拆)
type A3 = Awaited<number> // number
寫 async 函式時搭配 ReturnType,才能拿到真正的回傳值型別:
async function load() {
return { id: 1 }
}
type R = ReturnType<typeof load> // Promise<{ id: number }>
type Data = Awaited<ReturnType<typeof load>> // { id: number }
字串大小寫工具
四個內建的字串字面值轉換:
type Uppercase<T extends string>
type Lowercase<T extends string>
type Capitalize<T extends string>
type Uncapitalize<T extends string>
type A = Uppercase<'hello'> // 'HELLO'
type B = Capitalize<'hello'> // 'Hello'
配 mapped type + template literal 重新命名 key,就是上一篇結尾那招:
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
}
type UserSetters = Setters<{ name: string; age: number }>
// { setName: (v: string) => void; setAge: (v: number) => void }
速查表
| Utility | 等價 | 用途 |
|---|---|---|
Partial<T> | 全部欄位變 ? | update / patch 場景 |
Required<T> | 全部欄位變必填 | 移除 ? |
Readonly<T> | 全部欄位 readonly | immutable view |
Pick<T, K> | 挑選欄位 | 派生子集 |
Omit<T, K> | 排除欄位 | 派生補集 |
Record<K, V> | k-v 等值物件 | dict 形式 |
Exclude<T, U> | 從 union 剔除 | 篩除 |
Extract<T, U> | 從 union 取出 | 篩入 |
NonNullable<T> | 移除 null / undefined | 收斂 union |
ReturnType<T> | 函式回傳值型別 | 同步契約 |
Parameters<T> | 函式參數 tuple | reuse 簽章 |
Awaited<T> | 拆 Promise | async 結果型別 |
自訂常用工具
標準庫沒有、但社群很常見的幾個:
// 至少要有一個欄位
type AtLeastOne<T, K extends keyof T = keyof T> =
K extends keyof T ? Partial<T> & Pick<T, K> : never
// 互斥(這幾個欄位只能擇一)
type XOR<A, B> =
| (A & { [K in keyof B]?: never })
| (B & { [K in keyof A]?: never })
// 深 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
何時不要硬寫複雜型別
最後幾條提醒:
- 型別讀不懂的人,永遠比寫的人多
- 複雜型別會拖慢編譯,IDE 提示也會卡
- 太聰明的型別常常壓不住邊界 case,最後還是得加
as收尾
實務原則:型別應該幫助理解,不是炫技。能用 Pick / Omit 解決的,就不要動 mapped + conditional。
下一篇切到「執行期與型別系統互動」這條線——TS 怎麼靠 typeof、instanceof、自訂 type guard 在 runtime 縮小型別。前面提過的 discriminated union 會在那裡跟 narrowing 收束在一起。
Latest Updates
- 2026.06.11 Content updated
