こんにちは!TypeScriptの世界へようこそ。
日々の開発の中で、「この関数の引数は、渡さなくてもいい(オプショナルな)んだけど、もしオブジェクトを渡すのだとしたら、特定のプロパティだけは絶対に省略してほしくないんだよね……」という場面に遭遇したことはありませんか?
TypeScriptを学び始めたばかりの頃や、他の言語から移行してきたばかりの時期は、「`?`(オプショナル修飾子)を付けたら、中身が全部あやふやになってしまって、型チェックをすり抜けてバグになってしまった!」という壁にぶつかりがちです。
でも、安心してください。TypeScriptの型システムは非常に賢く、そして柔軟です。
今回は、「オプショナル(省略可能)でありながら、渡すときは特定のプロパティを確実に必須にする」ための美しい型定義のテクニックを、基本から一歩進んだ応用まで、順を追って分かりやすく解説します。
ここをマスターすれば、TypeScriptの型を自在に操る第一歩を踏み出せますよ。さあ、一緒に見ていきましょう!
—
1. そもそも何が問題になるの?(よくある失敗例)
まずは、よくある「おねがい型定義」の失敗例から見てみましょう。
例えば、ユーザーのプロフィールを更新する関数 `updateProfile` を作るとします。
// ユーザーの設定情報
interface UserSettings {
theme?: “light” | “dark”;
notifications?: {
email: boolean;
push: boolean;
};
}
// プロフィール更新関数(引数全体がオプショナル)
function updateProfile(settings?: UserSettings) {
console.log(“設定を更新しました:”, settings);
}
この設計の意図はこうです。
- 設定を何も更新しないなら、`updateProfile()` と引数なしで呼んでほしい。
- もし設定を変更するなら、`updateProfile({ theme: “dark” })` のように渡してほしい。
しかし、この定義には大きな罠があります。
// 😱 エラーは出ないが、中身が「空っぽのオブジェクト」
updateProfile({});
// 😱 notifications を渡しているのに、中身が「空っぽのオブジェクト」
updateProfile({
notifications: {} // email も push もないのにコンパイルが通ってしまう!
});
これでは、実行時に `settings.notifications.email` を参照しようとした瞬間に「`Cannot read properties of undefined`」でクラッシュしてしまいます。
なぜこれが起きてしまうのか?
TypeScriptにとって、`notifications?: { email: boolean; push: boolean }` という定義は、「`notifications` という鍵(キー)自体が存在しないかもしれないし、存在するならその値はオブジェクトだよ」という意味になります。
しかし、JavaScriptのオブジェクトの性質上、`notifications: {}`(中身が空のオブジェクト)を渡されると、TypeScriptは「オブジェクトとしては存在している(でも中身のプロパティは欠けている)」と解釈し、厳密なチェックが漏れてしまうことがあるのです。
—
2. 解決策その1:王道の「判別可能なユニオン型(Discriminated Unions)」
「引数を省略してもいいけれど、もし渡すならこの組み合わせじゃないとダメ!」を最もエレガントに解決するのが、TypeScriptの華である「判別可能なユニオン型」です。
型を「または(`|`)」で結合し、状況に応じたパターンをコンパイラに教えてあげましょう。
コード例:
// 1. 通知設定が「完全に存在する」場合の型
interface CompleteNotifications {
email: boolean;
push: boolean;
}
// 2. 引数オブジェクト全体の型を定義
type UpdateProfileOptions =
| { theme?: “light” | “dark”; notifications: CompleteNotifications } // パターンA: notificationsを渡すなら必須
| { theme: “light” | “dark”; notifications?: never } // パターンB: themeだけを渡す(notificationsは渡さない)
| Record
// 3. 関数を定義(引数をオプショナルにする)
function updateProfile(settings?: UpdateProfileOptions) {
if (!settings) {
console.log(“更新はありません”);
return;
}
// ここで安全に処理ができる!
}
どうしてこれで防げるの?
この型定義を導入すると、TypeScriptコンパイラは開発者の入力を厳しく見守ってくれるようになります。
// ✅ OK: 引数なし(オプショナルなのでOK)
updateProfile();
// ✅ OK: themeだけを渡す
updateProfile({ theme: “dark” });
// ✅ OK: 両方正しく渡す
updateProfile({
theme: “light”,
notifications: { email: true, push: false }
});
// ❌ エラー!: notificationsを渡しているのに、中身が足りない
updateProfile({
notifications: { email: true } // 🚨 エラー: ‘push’ プロパティが足りません!
});
「渡すなら、この形。渡さないなら、存在すらさせない」というルールを、型レベルで強制することに成功しました。
—
3. 解決策その2:`Required` と `Omit` の合わせ技
「もっとシンプルに、既存の型の一部分だけを『必須』に変換したい!」というときは、TypeScriptが標準で用意してくれているユーティリティ型を組み合わせるのがスマートです。
ここでは、以下の2つの道具を使います。
1. `Required
2. `Omit
視覚的なイメージ:
[元のゆるい型]
- theme (任意)
- email (任意)
- push (任意)
↓
[Omit で email と push を除外] + [Required で email と push を必須化して結合]
↓
[理想の型]
- theme (任意)
- email (必須!)
- push (必須!)
コード例:
interface RawSettings {
theme?: “light” | “dark”;
email?: boolean;
push?: boolean;
}
// email と push だけを「必須」にした新しい型を作る!
type StrictSettings =
Omit
& Required
// 引数はオプショナル(省略可能)にする
function saveSettings(settings?: StrictSettings) {
if (settings) {
// ここでは email と push は「絶対に存在する」と保証される!
console.log(settings.email.valueOf());
}
}
実行結果(コンパイル時の挙動):
// ✅ OK: 引数をまるごと省略する
saveSettings();
// ❌ エラー: オブジェクトを渡したのに、必須の email/push がない
saveSettings({ theme: “light” });
// 🚨 エラー: プロパティ ‘email’, ‘push’ は型 ‘{ theme: “light”; }’ にありません。
// ✅ OK: 必要なものを全て満たしている
saveSettings({ email: true, push: false });
この方法の素晴らしいところは、元の `RawSettings` という型定義を汚さずに、関数に渡す瞬間だけルールを厳しくカスタマイズできる点にあります。
—
4. 初学者が陥りがちな落とし穴:`undefined` との戦い
ここで、多くの開発者が一度は引っかかる「オプショナル引数」の落とし穴を紹介します。
// ⚠️ 惜しいけれど、ちょっと不便な定義
function setup(options: { theme: “light” | “dark” } | undefined) {
// …
}
この定義だと、関数を呼び出すときに必ず `undefined` という文字を書かなければならなくなります。
setup(); // 🚨 エラー: 引数が必要です!
setup(undefined); // ✅ これなら通るけれど、毎回書くのは面倒ですよね…
解決策:
必ず関数の引数定義の側に `?` をつけるか、デフォルト値を指定しましょう。
// ✅ 引数の後ろに `?` をつけることで、引数自体の省略が可能になります
function setup(options?: { theme: “light” | “dark” }) {
// …
}
setup(); // ✅ これでスッキリ呼び出せます!
—
5. まとめ:TypeScriptの型は「未来の自分へのラブレター」
今回ご紹介したテクニックをまとめましょう。
1. 基本のオプショナル(`?`)は便利ですが、オブジェクトの階層が深くなるとチェックが甘くなることがあります。
2. 「渡すならこのプロパティを必須にする」という高度な制御は、判別可能なユニオン型(Discriminated Unions)を使うのが最も安全で美しいアプローチです。
3. 既存の型を再利用したいときは、`Required` や `Omit` などのユーティリティ型をパズルのように組み合わせることで、すっきりとしたコードが書けます。
TypeScriptの型エラーは、私たちを邪魔する「敵」ではありません。むしろ、数ヶ月後の自分がコードを変更したときに、「そこを直すなら、こっちのプロパティも必要だよ!」と優しく教えてくれる未来への道標です。
一歩一歩、型システムの強力な味方につけて、安全で楽しい開発ライフを送ってくださいね。応援しています!