TypeScript utility types Partial Pick Omit Record

[TypeScript] 內建型別工具

上一篇講的泛型、keyofinfer、mapped、conditional 都是「素材」,這一篇看標準庫用這些素材組好的成品:PartialPickOmitRecordReturnType⋯⋯。會用是日常戰力;理解背後怎麼組的,遇到內建不夠用時才有辦法自己擴充。所以下面每個工具都會附上它的實作(或等價寫法),對照前一篇的機制看。

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 裡的 nullundefined

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>全部欄位 readonlyimmutable 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>函式參數 tuplereuse 簽章
Awaited<T>拆 Promiseasync 結果型別

自訂常用工具

標準庫沒有、但社群很常見的幾個:

// 至少要有一個欄位
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 怎麼靠 typeofinstanceof、自訂 type guard 在 runtime 縮小型別。前面提過的 discriminated union 會在那裡跟 narrowing 收束在一起。

Latest Updates

  • 2026.06.11 Content updated