【実務・中級編】 Partial – TypeScript実践ガイド

こんにちは。現場で日々TypeScriptの型パズルと格闘している君なら、一度や二度はお世話になっているはずだ。そう、`Partial`。

型 `T` のすべてのプロパティをごっそりオプショナル(省略可能)にしてくれる、あの便利なユーティリティ型だ。

「APIから返ってくるデータの一部だけを更新するパッチリクエスト(PATCH)の型定義に最高!」
「フォームの初期値を定義する時に便利!」

……と、ここまでは入門書や公式ドキュメントに書いてある通り。しかし、実務の現場では「なんとなく便利だから」と雑に使ってしまい、後々「あれ、このプロパティ、本当は必須のはずなのにundefinedが混ざってランタイムエラーになったぞ……?」というバグを踏み抜く中級エンジニアの姿を本当によく見かける。

今回は、この `Partial` の標準的な仕様から、TypeScriptの型システムとランタイムの裏側の関係、そして現場で本当に使える実践的なテクニックまで、シニアの視点で徹底的に解説しよう。

—

1. `Partial` の基本仕様と「裏側」の仕組み

まずは基本のおさらいだ。`Partial` は、TypeScriptの標準ライブラリ(`lib.es5.d.ts`)の中で、次のようなMapped Types(マップ型)として定義されている。

type Partial = {
[P in keyof T]?: T[P];
};

たったこれだけのコードだが、やっていることは非常に強力だ。
1. `keyof T` で型 `T` のすべてのプロパティ名(キー)をユニオン型として抽出する。
2. `in` 演算子でそのキーをループ(マップ)する。
3. `?` を付与することで、すべてのプロパティをオプショナル(`undefined` を許容する状態)に書き換える。

⚠️ ブラウザ(ランタイム)の裏側はどうなっているのか?

ここでエンジニアとして一歩踏み込んで考えてほしい。この `Partial` を使った型定義は、ブラウザが実行するJavaScriptのコードに、一体どんな影響を与えるだろうか?

答えは「何の影響も与えない」だ。

TypeScriptの型はすべて、ビルド時(コンパイル時)に綺麗に消し去られる(Erasure)。ブラウザが動かすのは、純粋なJavaScriptのコードだけだ。
つまり、`Partial` を使って型の上で「このプロパティは省略可能だ」と宣言しても、ランタイムで自動的にデフォルト値が入るわけでも、オブジェクトからプロパティが消去されるわけでもない。

型定義はあくまで「開発中のIDEの補完」と「コンパイル時の型チェック」のためのもの。この大前提を忘れると、「型エラーが出ないから大丈夫」と油断して、実行時エラーの爆弾を抱えることになる。

—

2. 現場で即コピペできる!実践的なコード例

では、実際のフロントエンド開発で `Partial` をどう使いこなすべきか。よくあるユースケースとして、「ユーザープロフィールの部分更新(PATCH)」を題材に、キレイなコードを見ていこう。

/

  • ユーザーの基本情報(データベースの完全なレコードを想定)

/
interface UserProfile {
id: string;
username: string;
email: string;
bio: string;
age: number;
}

/

  • 【NG例】雑に全体を Partial にしてしまうケース
  • これだと、絶対に書き換えてほしくない ‘id’ までオプショナルになり、
  • さらに何を更新しようとしているのか意図が曖昧になる。

/
// type BadUpdateUserDto = Partial;

/

  • 【Good例】Omit と Partial を組み合わせた堅牢なアプローチ
  • 1. 変更不可な ‘id’ は Omit で除外する
  • 2. 残りのプロパティを Partial でオプショナルにする

/
type UpdateUserProfilePayload = Partial>;

// — 実装イメージ —

/

  • ユーザープロフィールを更新するAPIクライアント関数
  • @param userId 更新対象のユーザーID(識別子は必ず別途必須として持たせる)
  • @param payload 変更したいフィールドだけを詰めたオブジェクト

/
async function updateUserProfile(
userId: string,
payload: UpdateUserProfilePayload
): Promise {

// 実際のフェッチ処理
const response = await fetch(`/api/users/${userId}`, {
method: ‘PATCH’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error(‘プロフィールの更新に失敗しました’);
}

return response.json();
}

// — 使用例 —

// OK: メールアドレスと自己紹介だけを更新する場合
const patchData: UpdateUserProfilePayload = {
email: ‘new-ishiki@example.com’,
bio: ‘TypeScriptを愛するフロントエンドエンジニアです。’,
};

updateUserProfile(‘user_12345’, patchData);

このパターンの何が素晴らしいのか?

1. 変更してはいけない主キー(`id`)を型レベルで保護している
`Partial` をそのまま使うと、うっかり `id` まで書き換えるペイロードを作れてしまうが、`Omit` と組み合わせることでそれをコンパイルエラーとして防げる。
2. APIの仕様(PATCHリクエスト)と型が美しく1対1で対応する
バックエンドの「ID指定はパスパラメータで行い、リクエストボディには変更分だけを乗せる」というRESTの設計思想に、TypeScriptの型が完璧に寄り添っている。

—

3. シニアが教える「`Partial` の罠」と回避策

最後に、実務で多くのエンジニアがハマる `Partial` の「深淵」について話しておこう。

罠:ネストしたオブジェクト(Deep Partial)問題

`Partial` は、直下のプロパティしかオプショナルにしてくれない(浅い / Shallow)。次のようなネストした設定オブジェクトがあった場合を見てほしい。

interface AppConfig {
theme: {
mode: ‘light’ | ‘dark’;
accentColor: string;
};
pagination: {
limit: number;
offset: number;
};
}

// Partial の実質的な構造:
// {
// theme?: { mode: ‘light’ | ‘dark’; accentColor: string; }; // ← theme 自体は省略可能だが、中の mode は省略不可!
// pagination?: { limit: number; offset: number; };
// }

「設定の一部だけを渡したいから `Partial` を使おう!」とした時、`theme` オブジェクトを渡すなら、その中の `mode` と `accentColor` を両方とも書か強制されるという罠に直面する。

💡 解決策:DeepPartial を自作する(または既存のライブラリを使う)

階層の深いオブジェクトのすべてのプロパティを再帰的にオプショナルにしたい場合は、次のような再帰型(Recursive Type)を定義するのが、シニアの現場の常道だ。

/

  • オブジェクトのネスト構造の奥底まですべてオプショナルにするユーティリティ型

/
type DeepPartial = {
[P in keyof T]?: T[P] extends object
? DeepPartial
: T[P];
};

// これを使えば、以下のようにネストした一部だけを安全に渡せるようになる
const myConfig: DeepPartial = {
theme: {
mode: ‘dark’, // accentColor を書かなくてもエラーにならない!
},
};

(※実務では、`utility-types` などの信頼できるサードパーティライブラリからインポートして使うことも多い)

—

まとめ

`Partial` は非常にシンプルで使いやすい反面、そのシンプルさゆえに「どこまでオプショナルにすべきか」という設計の意図がボヤけやすい型でもある。

  • ただの思考停止で `Partial` を使うな。 `Omit` や `Pick` と組み合わせて、本当にオプショナルであるべき範囲を絞り込もう。
  • ネスト構造には要注意。 深い階層のデータには再帰的なアプローチ(`DeepPartial`)を検討しよう。
  • 型はランタイムを救わない。 型でオプショナルにしても、実行時のバリデーション(ZodやValibotなど)と組み合わせることで初めて本当の意味で堅牢なアプリケーションになることを忘れないでほしい。

今日のこの知識を武器に、君の書くTypeScriptコードをさらに一歩、洗練されたものにしてくれ。応援しているぞ!

コメント

タイトルとURLをコピーしました