TypeScript 工具型別 (Utility Types)

TypeScript 內建了許多全域的「工具型別 (Utility Types)」。你可以把它們想像成是「專門處理型別的函式」:你輸入一個型別,它幫你加工處理後,吐出一個新的型別。

掌握這些工具型別非常重要,因為它們可以幫助你:

  1. 減少重複定義:不需要為了微小的差異(例如一個欄位變可選)就重新寫一個介面。
  2. 保持型別的一致性:當原始型別修改時,衍生出來的型別會自動更新。

屬性修飾工具

這類工具主要用來修改介面中屬性的特性(例如是否選填、是否唯讀)。

Partial<T>

用途:將型別 T 的所有屬性都變成「可選的 (optional)」。

使用場景:最常見的就是在做「更新」操作時。當我們要更新一個使用者的資料,我們可能只想更新他的名字,而不需要連同 Email 一起傳送。

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

// 原始做法:手動重寫一個 interface
// interface PartialUser {
//   id?: number;
//   name?: string;
//   email?: string;
// }

// 使用 Partial:自動產生上述結構
type PartialUser = Partial<User>;

function updateUser(id: number, updates: Partial<User>) {
  // ...更新邏輯
}

// OK:只傳送 name
updateUser(1, { name: '小明' });

// OK:什麼都不傳 (因為所有屬性都變 optional 了)
updateUser(2, {});

Required<T>

用途:將型別 T 的所有屬性都變成「必填的 (required)」,也就是移除 ? 修飾符。

使用場景:通常用於處理設定檔。我們定義設定介面時,允許使用者只填部分設定(其他用預設值),但在程式內部邏輯中,我們希望能確保所有設定值都已經補齊了。

interface Config {
  host?: string;
  port?: number;
  timeout?: number;
}

// 假設這是我們的預設設定
const defaultConfig = {
  host: 'localhost',
  port: 8080,
  timeout: 5000,
};

// 經過合併處理後,我們確定回傳的設定一定包含所有欄位
function processConfig(userConfig: Config): Required<Config> {
  return { ...defaultConfig, ...userConfig };
}

const finalConfig = processConfig({ port: 3000 });
// 這裡 finalConfig.host 一定是 string,不會是 undefined
console.log(finalConfig.host);

Readonly<T>

用途:將型別 T 的所有屬性都變成「唯讀 (readonly)」。

使用場景:當你希望確保某個物件在傳遞過程中不會被意外修改時(類似 React 的 props 或 state,或者 Redux 的 store)。

interface Todo {
  title: string;
}

const todo: Readonly<Todo> = {
  title: '寫程式',
};

// todo.title = '睡覺';
// 錯誤!無法指派給 'title',因為它是唯讀屬性

結構選取與排除工具

這類工具用來從現有的型別中「挑選」或「剔除」某些屬性,產生新的型別。

Pick<T, K>

用途:從型別 T 中,「挑選」出一組屬性 K 來建立新型別。

使用場景:當你要從一個巨大的介面中,只取出一小部分欄位來用時。例如:從完整的 User 物件中,只提取出用來顯示在列表上的欄位。

interface User {
  id: number;
  name: string;
  email: string;
  address: string;
  phone: string;
  // ... 還有很多欄位
}

// 我們只想在畫面上顯示名字和 Email
type UserPreview = Pick<User, 'name' | 'email'>;

// UserPreview 等同於:
// {
//   name: string;
//   email: string;
// }

Omit<T, K>

用途:這剛好跟 Pick 相反。從型別 T 中,「排除」掉一組屬性 K,保留剩下的。

使用場景:當你要移除某些敏感資訊(如密碼),或是在繼承介面時想移除某些不適用的欄位。

interface User {
  id: number;
  name: string;
  password: string; // 敏感資訊
  isAdmin: boolean; // 敏感資訊
}

// 傳給前端的資料,要過濾掉 password 和 isAdmin
type UserDto = Omit<User, 'password' | 'isAdmin'>;

// UserDto 等同於:
// {
//   id: number;
//   name: string;
// }

小技巧:選取欄位少時用 Pick,選取欄位多(只想扣掉一兩個)時用 Omit


物件映射工具

Record<K, T>

用途:建立一個物件型別,其所有的 Key 都是 K 型別,所有的 Value 都是 T 型別。

使用場景:非常適合用來定義「字典 (Dictionary)」或「映射表 (Map)」結構。

type Page = 'home' | 'about' | 'contact';

interface PageInfo {
  title: string;
  url: string;
}

// 我們希望定義一個物件,它的 key 只能是 'home' | 'about' | 'contact'
// 而 value 必須是 PageInfo
const nav: Record<Page, PageInfo> = {
  home: { title: '首頁', url: '/' },
  about: { title: '關於我們', url: '/about' },
  contact: { title: '聯絡資訊', url: '/contact' },
  // other: { ... } // 錯誤!'other' 不在 Page 定義的 key 之中
};

聯合型別 (Union) 處理工具

這類工具專門用來處理 type A = "a" | "b" | "c" 這種聯合型別。

Exclude<T, U>

用途:從聯合型別 T 中,「剔除」掉可以指派給 U 的部分。(簡單記法:T - U)。

type Status = 'success' | 'error' | 'loading';

// 我想要一個不包含 'loading' 的狀態
type FinishedStatus = Exclude<Status, 'loading'>;
// 結果:'success' | 'error'

Extract<T, U>

用途:從聯合型別 T 中,「提取」出可以指派給 U 的部分,也就是 T 和 U 的交集。

type Shape = 'circle' | 'square' | 'triangle';
type TwoDShape = 'circle' | 'square' | 'line';

// 取出兩邊都有的形狀
type CommonShape = Extract<Shape, TwoDShape>;
// 結果:'circle' | 'square'

NonNullable<T>

用途:從型別 T 中剔除 nullundefined

type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>;
// 結果:string

函式相關工具

這類工具非常強大,常用於當你使用第三方套件,該套件沒有直接匯出某個型別,但你有那個函式時。

ReturnType<T>

用途:取得一個函式的回傳值型別

使用場景:Redux 的 Action Creator,或者任何工廠函式。

function createUser() {
  return {
    id: 1,
    name: '小明',
    settings: { theme: 'dark', notifications: true },
  };
}

// 假設我們想拿到 settings 的型別,但原本沒定義 interface
// 我們可以這樣做:

// 1. 先拿到函式回傳的整個物件型別
type UserObject = ReturnType<typeof createUser>;

// 2. 再從裡面抓出 settings
type UserSettings = UserObject['settings'];

Parameters<T>

用途:取得一個函式的參數型別(回傳的是一個 Tuple)。

function add(x: number, y: number): number {
  return x + y;
}

type AddParams = Parameters<typeof add>;
// 結果:[x: number, y: number]

// 甚至可以抓出第一個參數的型別
type FirstArg = AddParams[0]; // number

字串操作工具 (TypeScript 4.1+)

這些是用來處理字串 Literal Types 的,通常搭配 Template Literal Types 使用。

  • Uppercase<S>: 轉大寫
  • Lowercase<S>: 轉小寫
  • Capitalize<S>: 首字大寫
  • Uncapitalize<S>: 首字小寫
type Event = 'click' | 'change';

// 自動產生 'onClick' | 'onChange'
type HandlerName = `on${Capitalize<Event>}`;

總結

工具型別是 TypeScript 讓程式碼更簡潔、更 DRY 的利器。

  • 要修改屬性?找 Partial, Required, Readonly
  • 要篩選欄位?找 Pick, Omit
  • 要定義對照表?找 Record
  • 要處理 Union?找 Exclude, Extract
  • 要從函式反推型別?找 ReturnType, Parameters

熟練這些工具,你的 TypeScript 功力將會提升一個檔次!