本文 AI 產出,尚未審核

TypeScript × React:Props 的 OmitPartial 操作實務應用


簡介

在 React 開發中,Component Props 是元件與外部溝通的唯一管道。隨著專案規模擴大,元件的 Props 介面往往會變得相當龐大,導致以下兩個常見問題:

  1. 重複定義:多個元件共享相似的屬性時,需要手動複製與維護相同的型別。
  2. 彈性不足:有時候只想在某些情境下「省略」或「變更」部分屬性為可選,卻必須重新寫一個全新的介面。

TypeScript 提供的 Utility Types(例如 OmitPartial)正是為了解決這類問題而設計的。透過它們,我們可以在不破壞原始型別的前提下,快速產生「省去某些屬性」或「全部屬性變為可選」的 Props 版本,讓元件更具彈性、程式碼更易維護。

本文將從概念講起,搭配實作範例,說明在 React + TypeScript 專案中如何正確使用 OmitPartial,並探討常見陷阱與最佳實踐,最後提供幾個實務情境的應用示例。


核心概念

1. Partial<T>:把所有屬性變為可選

Partial<T> 會遍歷型別 T 的每個屬性,將它們的修飾子從必填 (required) 轉為可選 (optional)。

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

// 變為可選的 Props
type UserPartial = Partial<User>;

UserPartial 等同於:

{
  id?: number;
  name?: string;
  email?: string;
}

在 React 中,常用於 表單編輯分步驟 UI,因為使用者可能只填寫部分欄位,我們不想每次都寫一長串 ?

2. Omit<T, K>:從型別中剔除指定屬性

Omit<T, K> 接收兩個參數:原始型別 T 與欲剔除的屬性鍵 K(可以是單一鍵或聯集)。它會返回一個 不包含 K 的新型別。

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

// 去掉 onClick,交給外層包裝元件自行處理
type SimpleButtonProps = Omit<ButtonProps, "onClick">;

SimpleButtonProps 只剩下 labeldisabled,適合 高階元件(HOC)容器元件 只關心外觀,不處理事件邏輯的情境。

3. Pick<T, K>:只挑選需要的屬性(與 Omit 常一起使用)

雖然題目沒有要求,但在實務上常會配合 Pick 使用,形成「只保留」或「剔除」的雙向操作。

type ButtonLabelProps = Pick<ButtonProps, "label">;

4. 結合 PartialOmit

有時候我們想 剔除某些屬性,同時 把剩餘屬性變為可選,可以這樣寫:

type EditableUserProps = Partial<Omit<User, "id">>;

這個型別的含義是:除了 id 必須保留外,其他欄位皆為可選,非常適合「編輯表單」的情境。


程式碼範例

以下示範 5 個在 React + TypeScript 專案中常見的 Omit / Partial 用法,並配上詳細註解說明。

範例 1:表單編輯的 Partial Props

// UserForm.tsx
import React, { useState } from "react";

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

/**
 * 把 User 的所有屬性變為可選,讓表單只需要提供
 * 使用者實際填寫的欄位。
 */
type UserFormProps = Partial<User> & {
  /** 提交表單時的回呼 */
  onSubmit: (data: User) => void;
};

export const UserForm: React.FC<UserFormProps> = ({
  id,
  name,
  email,
  age,
  onSubmit,
}) => {
  const [form, setForm] = useState<User>({
    id: id ?? 0,
    name: name ?? "",
    email: email ?? "",
    age,
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = () => onSubmit(form);

  return (
    <div>
      {/* 只要傳入的 props 有值,就會預設顯示;否則為空字串 */}
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="age" value={form.age?.toString() ?? ""} onChange={handleChange} />
      <button onClick={handleSubmit}>送出</button>
    </div>
  );
};

重點Partial<User>UserForm新增編輯 兩種情境下都能共用,同時保留 onSubmit 為必填。


範例 2:高階元件 (HOC) 透過 Omit 去除事件屬性

// withDisabled.tsx
import React from "react";

type WithDisabledProps<P> = Omit<P, "onClick"> & {
  /** HOC 會自行處理點擊事件 */
  disabled?: boolean;
};

export function withDisabled<P extends { onClick: () => void }>(
  WrappedComponent: React.ComponentType<P>
) {
  return (props: WithDisabledProps<P>) => {
    const { disabled, ...rest } = props as any;

    const handleClick = () => {
      if (disabled) return;
      // 交給原始元件的 onClick
      (props as P).onClick?.();
    };

    return <WrappedComponent {...(rest as P)} onClick={handleClick} />;
  };
}

/* 使用範例 */
interface ButtonProps {
  label: string;
  onClick: () => void;
  style?: React.CSSProperties;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, style }) => (
  <button style={style} onClick={onClick}>
    {label}
  </button>
);

export const SafeButton = withDisabled(Button);

說明Omit<P, "onClick"> 移除原本的 onClick,讓 HOC 接管點擊邏輯,並在 disabledtrue 時阻止觸發。


範例 3:組合型別 – 只保留 UI 相關屬性

// Card.tsx
import React from "react";

interface CardBaseProps {
  title: string;
  content: string;
  /** 只在 UI 層需要的屬性 */
  backgroundColor?: string;
  /** 行為層的屬性 */
  onClose: () => void;
}

/**
 * 只要 UI 組件本身,不需要 onClose,就可以用 Omit 產生
 * 只關注外觀的型別,供子元件或 Storybook 使用。
 */
export type CardUIProps = Omit<CardBaseProps, "onClose">;

export const Card: React.FC<CardBaseProps> = ({
  title,
  content,
  backgroundColor,
  onClose,
}) => (
  <div style={{ backgroundColor }}>
    <h2>{title}</h2>
    <p>{content}</p>
    <button onClick={onClose}>關閉</button>
  </div>
);

應用:在 Storybook 中,我們只需要提供 CardUIProps,不必模擬 onClose 行為,讓測試更聚焦於 UI。


範例 4:Partial + Omit 結合:編輯模式的 Props

// EditUserModal.tsx
import React from "react";

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "member";
}

/**
 * 編輯 Modal 只需要讓 id 必填,其他欄位皆可選。
 * 這正好符合 Partial<Omit<User, "id">> + { id: number } 的需求。
 */
type EditUserProps = { id: number } & Partial<Omit<User, "id">>;

export const EditUserModal: React.FC<EditUserProps> = ({
  id,
  name,
  email,
  role,
}) => {
  // 假設從 API 取得完整資料後再填入表單
  // 這裡僅示範 UI 結構
  return (
    <div>
      <h3>編輯使用者 #{id}</h3>
      <input defaultValue={name} placeholder="姓名" />
      <input defaultValue={email} placeholder="Email" />
      <select defaultValue={role}>
        <option value="admin">管理員</option>
        <option value="member">一般會員</option>
      </select>
    </div>
  );
};

說明Partial<Omit<User, "id">>id 以外的屬性全變可選,配合 { id: number },即完成「只需要 id,其他自行決定」的需求。


範例 5:利用 Partial 建立可重用的通用 Props

// withLoading.tsx
import React from "react";

interface LoadingProps {
  isLoading: boolean;
  /** 任何其他元件可能需要的屬性 */
  [key: string]: any;
}

/**
 * Partial<LoadingProps> 讓使用者在包裝元件時
 * 可以只提供 isLoading,其他屬性保持原樣。
 */
export function withLoading<P>(Component: React.ComponentType<P>) {
  return (props: Partial<LoadingProps> & P) => {
    const { isLoading, ...rest } = props as LoadingProps;

    if (isLoading) return <div>載入中…</div>;

    return <Component {...(rest as P)} />;
  };
}

/* 使用範例 */
interface DataListProps {
  items: string[];
}

const DataList: React.FC<DataListProps> = ({ items }) => (
  <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>
);

export const DataListWithLoading = withLoading(DataList);

重點Partial<LoadingProps>isLoading 變為可選,若未傳入則預設為 false,提升 HOC 的彈性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
過度使用 any Omit/Partial 結合時,若未正確指定泛型,TypeScript 可能推斷為 any,失去型別保護。 永遠 為泛型提供明確的型別參數,例如 Omit<Props, "onClick">
遺失必填屬性 Partial<T> 會把 所有 屬性變為可選,若不小心把本應必填的 id 也變可選,會導致 runtime 錯誤。 只對需要的子集合使用 Partial<Omit<T, "id">>,或自行建立 Required<T> 再組合。
屬性衝突 使用 Omit<T, K> & Partial<T> 可能產生同名屬性的衝突,讓型別變得難以預測。 分離 兩個步驟:先 OmitPartial,或使用 Pick 明確挑選剩餘屬性。
高階元件的 Props 推斷失敗 HOC 中若直接使用 props as any,會失去型別檢查。 使用 泛型約束(如 <P extends { onClick: () => void }>)並在返回值中明確標註 React.ComponentType<P>
預設值與 Partial 的衝突 在使用 Partial 的元件內部直接解構必填屬性,若未提供預設值會出現 undefined useStatedefaultProps 中提供合理的 fallback,或使用 ?? 進行空值合併。

最佳實踐

  1. 盡量在最外層介面保留完整型別,只在需要彈性的地方使用 Partial / Omit
  2. 為 Utility Types 加上說明性註解,讓團隊成員快速了解意圖(例如 /** 編輯模式下的 Props */)。
  3. 在 HOC 中使用泛型約束,避免 any 洩漏。
  4. 結合 PickOmit,形成「白名單」或「黑名單」的明確策略。
  5. 使用 as const字面量類型,確保 keyof 推斷正確,例如 type Keys = keyof typeof SOME_CONST;

實際應用場景

場景 為何需要 Omit / Partial 範例概述
表單新增/編輯共用元件 編輯時只需要傳入已存在的資料,新增時則全為空。 type FormProps = Partial<User> & { onSubmit: ... }
高階元件統一處理 Loading / Error HOC 只需要提供 isLoading,其他 Props 直接傳遞。 withLoading(Component) 中使用 Partial<LoadingProps>
Storybook 測試 UI 測試時不想提供行為屬性(如 onClick),只關注外觀。 type CardUIProps = Omit<CardBaseProps, "onClose">
多語系或權限系統 不同使用者角色只需要部分屬性,且某些屬性在某些角色下不可見。 `type AdminProps = Pick<User, "id"
動態表格欄位 表格欄位可根據需求開關,使用 Partial 讓每個欄位屬性可選。 type TableColumn = Partial<{ title: string; sortable: boolean; width: number }>

總結

  • **Partial<T>** 讓所有屬性變為可選,適合「表單編輯」或「可變動 UI」的情境。
  • **Omit<T, K>** 剔除指定屬性,常用於 高階元件Storybook權限過濾
  • 兩者可以組合使用(如 Partial<Omit<T, "id">>),打造彈性十足的 Props 型別。
  • 實務上,避免過度泛化,只在需要的地方使用 Utility Types,並加入說明性註解與泛型約束,以保持型別安全與程式碼可讀性。

透過本文的概念與範例,你應該已能在日常的 React + TypeScript 開發中,熟練運用 OmitPartial 來簡化 Props 定義、提升元件彈性、減少重複程式碼,進而寫出更乾淨、可維護的前端程式碼。祝開發順利!