お疲れ。最近、君が書いたフォームバリデーションの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
// 内部でバックエンドへ 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
2. 「とりあえず `Partial`」病
何でもかんでも `Partial` で受けて内部でガードするコードを書く奴がいるが、それはAPIの契約(インターフェース)が曖昧な証拠だ。本当に一部の更新なのか、それとも必須データが抜けているバグなのかを見極めろ。
—
まとめ
どうだ? `Partial`、`Required`、`Readonly` は、ただの「便利なユーティリティ」ではなく、君のコードの意図を正確にコンパイラに伝え、バグの温床をビルド時に潰すための優秀なガードマンだということが分かったはずだ。
明日からのコードレビューでは、無駄に新しい型を定義していないか、あるいは `any` で逃げていないか、これらのユーティリティ型でスマートに解決できないかを意識してみてくれ。
君ならもっといいコードが書けるはずだ。期待しているぞ。さて、次のタスクに取り掛かろうか。

コメント