【実務・中級編】 基本ユーティリティ型 (Partial, Required, Readonly) – TypeScript実践ガイド

お疲れ。最近、君が書いたフォームバリデーションのPRを見たんだが……おっと、悪口じゃない。コード自体は動くし、テストもパスしている。ただ、型定義のところで少し「もったいない」書き方をしている箇所があった。

例えば、APIから返ってきたユーザーデータの更新処理。元の型をそのまま流用したがために、すべてのプロパティを毎回無理やり渡していなかったか? あるいは、ガチガチに固めた設定オブジェクトの一部だけを上書きしたくて、泣く泣く `as any` で逃げたとか。

……心当たりがありそうだな。よし、いい機会だ。今回はフロントエンドの実務で避けて通れない、`Partial`、`Required`、`Readonly` という「基本の3大ユーティリティ型」について徹底的に叩き込んでやろう。

公式ドキュメントを読めば「プロパティをオプショナルにする」「必須にする」「読み取り専用にする」と一言で書いてある。だが、「裏側でTypeScriptの型エンジンがどう動いているか」「なぜ実務のコードベースでこれらが必須の防波堤になるのか」 を理解しているかどうかで、君の書くコードの「格」が何段階も変わる。

さあ、コーヒーでも飲みながら聞いてくれ。

—

1. なぜ「素の型」だけでは実務で戦えないのか?

TypeScriptを書き始めた頃は、だいたいこんなインターフェースを定義して満足する。

interface UserProfile {
id: string;
name: string;
email: string;
age: number;
}

完璧な型だ。だが、現実は非情だ。
プロフィール編集画面(フォーム)を作るとき、ユーザーが変更するのはせいぜい `name` か `email` くらい。バックエンドの更新API(`PATCH` リクエスト)に送るデータは、当然こうしたいはずだ。

// PATCH /users/1 へのリクエストボディ
{
name: “新しい名前”
// id, email, age は送らないこともある
}

ここで「じゃあ更新用の型を別で作るか」と `interface UpdateUserProfile` を新しく定義し始めたら、それは負けフラグだ。元の `UserProfile` にプロパティが追加・変更されるたびに、更新用の型も手動でメンテする羽目になる。DRY原則(Don’t Repeat Yourself)の完全な崩壊だな。

ここで登場するのが、TypeScriptが最初から用意してくれている魔法の杖、ユーティリティ型だ。

—

2. 内部実装を覗く:TypeScriptは裏側で何をしているのか?

「マジックのように型を変えてくれる便利機能」で終わらせるな。シニアを目指すなら、これらのユーティリティ型が内部でどう書かれているかを知るべきだ。

実は、これらはTypeScriptの「Mapped Types(マッピング型)」と「Conditional Types(条件付き型)」、そして「Modifiers(修飾子操作)」を組み合わせた、たった数行のコードで書かれている。

実際の型定義のソースコードを覗いてみよう。

/

  • Partial の正体

/
type MyPartial = {
[K in keyof T]?: T[K];
};

/

  • Required の正体

/
type MyRequired = {
[K in keyof T]-?: T[K];
};

/

  • Readonly の正体

/
type MyReadonly = {
readonly [K in keyof T]: T[K];
};

どうだ? 意外とシンプルだろう。
ここで使われている構文を分解して解説しておこう。

1. `keyof T`: オブジェクト型 `T` のすべてのキーをユニオン型として抽出する(例: `”id” | “name” | “email” | “age”`)。
2. `in`: そのキーをループして新しいオブジェクトのキーを再構築する。
3. `?` と `-?`:

  • `?`: オプショナル(省略可能)にする。`Partial` で使われている。
  • `-?`: 「マイナス修飾子」と呼ばれ、元からあるオプショナルを剥ぎ取って強制的に必須(Required)にする。`Required` で使われている。

4. `readonly`: 読み取り専用にする。

つまり、ユーティリティ型とは「既存の型をゴリゴリ手動で書き直すんじゃなくて、コンパイラにルールを与えて自動変形させるためのスクリプト」なのだ。ブラウザのランタイム(実行時)にはこれらの型情報はすべて消え去り、純粋なJavaScriptのオブジェクトになる。だが、開発中のエディタ(TypeScript Language Server)がこの型情報を元に、僕たちのミスをリアルタイムで防いでくれている。

—

3. 現場で使える!実践コードとデザインパターン

理屈はこれくらいにして、明日からすぐに使える実践的なコードを見ていこう。

パターンA: `Partial` で安全なフォーム状態管理とAPIリクエストを作る

Reactなどのフロントエンドで、フォームの入力値や部分的な更新を扱うときのベストプラクティスだ。

interface Article {
id: string;
title: string;
content: string;
published: boolean;
tags: string[];
}

/

  • 記事のドラフト保存・部分更新を行う関数
  • すべてのフィールドが必須ではなくなるため、変更したい部分だけを渡せる

/
async function updateArticle(articleId: string, partialData: Partial

): Promise {
// 内部でバックエンドへ PATCH リクエストを送る処理を想定
console.log(`Updating ${articleId} with:`, partialData);
}

// 使用例:タイトルだけ変えたいときは、他のプロパティを書かなくても型エラーにならない!
updateArticle(“art_123”, {
title: “TypeScriptのユーティリティ型を極める夜”
});

もしここで `Partial` を使わずに素の `Article` 型を渡そうとしたら、`content` や `published` も書けとTypeScript怒られてしまう。かといって `any` に逃げたら、タイポ(例: `titel: “…”`)したときにバグが実行時まで隠れてしまう。それを防ぐのが `Partial` の美しさだ。

パターンB: `Required` で「設定値のデフォルト補完」を完璧に行う

次は逆に、すべてがオプショナルな設定オブジェクトに対して、デフォルト値をマージしたあとの「完全体」の型を保証するパターンだ。

interface EditorConfig {
theme?: “light” | “dark”;
fontSize?: number;
wordWrap?: boolean;
}

/

  • ユーザーの設定を受け取り、デフォルト値を適用して返す関数
  • 返り値の型を Required にすることで、
  • 「この関数を通過した後の設定オブジェクトは、すべてのプロパティが絶対に存在する」と保証できる

/
function initializeEditor(userConfig: EditorConfig): Required {
const defaultConfig: Required = {
theme: “dark”,
fontSize: 14,
wordWrap: true,
};

// スプレッド構文で上書き
return {
…defaultConfig,
…userConfig,
};
}

const myConfig = initializeEditor({ fontSize: 16 });

// myConfig.theme は “dark” または “light” (undefinedの可能性はゼロ!)
console.log(myConfig.theme.toUpperCase());

実務では、UIライブラリのテーマ設定や、axiosなどのHTTPクライアントの初期化処理でこのテクニックが頻出する。`undefined` のチェック地獄からコードを解放してくれる強力な武器だ。

パターンC: `Readonly` で「不変性(Immutability)」をコンパイル時に担保する

フロントエンドの状態管理(Redux ToolkitやReactのStateなど)では、データのイミュータビリティ(書き換え不可)が命だ。うっかり `state.user.name = “hoge”` のように直接書き換えてしまい、再描画が走らずに頭を抱えた経験はないだろうか?

`Readonly` を使えば、そんなうっかりミスをコンパイルエラーとして即座に検知できる。

interface AppState {
readonly currentUser: {
readonly id: string;
name: string; // ここをうっかり書き換えさせたくないとする
};
readonly items: string[];
}

// または、オブジェクト全体を一括でイミュータブルにしたい場合:
type SafeAppState = Readonly;

function processState(state: SafeAppState) {
// コンパイルエラー! Readonlyなのでプロパティの書き換えは許されない
// state.currentUser.name = “piyo”;

// 配列の要素を追加するメソッド(pushなど)も、ReadonlyArrayであれば型エラーになる
// state.items.push(“new-item”);

console.log(state.currentUser.id);
}

「定数(`const`)」は変数への再代入を防ぐものだが、オブジェクトの「中身(プロパティ)」の変更までは防げない。オブジェクトの中身までガチガチにイミュータブルにしたいときは、`Readonly` の出番というわけだ。

—

4. シニアが教える、実務でのアンチパターンと注意点

最後に、現場でやりがちな「危うい使い方」についても釘を刺しておこう。

1. `Partial` の安易なネストによる型安全性の崩壊
`Partial` のように、深くまであるオブジェクトに単純な `Partial` を使うと、1階層目しかオプショナルにならず、2階層目以降でまたエラーが出てハマることがある。深い階層まですべてオプショナルにしたい場合は、再帰型(Recursive Partial)を使う必要がある(これはいずれまた別の機会に教えよう)。
2. 「とりあえず `Partial`」病
何でもかんでも `Partial` で受けて内部でガードするコードを書く奴がいるが、それはAPIの契約(インターフェース)が曖昧な証拠だ。本当に一部の更新なのか、それとも必須データが抜けているバグなのかを見極めろ。

—

まとめ

どうだ? `Partial`、`Required`、`Readonly` は、ただの「便利なユーティリティ」ではなく、君のコードの意図を正確にコンパイラに伝え、バグの温床をビルド時に潰すための優秀なガードマンだということが分かったはずだ。

明日からのコードレビューでは、無駄に新しい型を定義していないか、あるいは `any` で逃げていないか、これらのユーティリティ型でスマートに解決できないかを意識してみてくれ。

君ならもっといいコードが書けるはずだ。期待しているぞ。さて、次のタスクに取り掛かろうか。

コメント

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