こんにちは!TypeScriptの型システムを旅する皆さん、チーフアーキテクトの私です。
今回は、TypeScriptの型システムにおいて、中級者へのステップアップとして絶対に避けて通れない、しかし最高の武器になる重要機能「Exact Optional Property Types(厳密なオプショナルプロパティ型)」についてお話しします。
「オプショナル(省略可能)な引数やプロパティに、`undefined`をあえて渡したいときってありますよね?」
「あれ、でもTypeScriptって、設定次第でその挙動がガラリと変わるって知っていましたか?」
ここをクリアすれば、あなたの書くコードの安全性と予測可能性は一段と跳ね上がります。ぜひ最後までついてきてくださいね。
—
1. そもそも「オプショナルプロパティ」のデフォルトの挙動はどうなっている?
まずは、TypeScriptの基本のおさらいから始めましょう。
オブジェクトの型定義や関数の引数で、`?` を使うと「そのプロパティは省略してもいいですよ(=オプショナル)」という意味になりますよね。
type UserOptions = {
name: string;
age?: number; // age は省略可能
};
この `age?: number` という定義があるとき、TypeScriptのデフォルト(`exactOptionalPropertyTypes: false`)の世界では、以下のようなコードはすべて「型エラーにならずに合格」します。
// 1. 普通にプロパティを省略する(これは当然OK)
const userA: UserOptions = { name: “Alice” };
// 2. ちゃんと数字を渡す(これも当然OK)
const userB: UserOptions = { name: “Bob”, age: 30 };
// 3. あえて `undefined` を明示的に代入する
const userC: UserOptions = { name: “Charlie”, age: undefined };
「あれ? 3番目の `age: undefined` もエラーにならないの? 普通じゃん」と思いましたか?
ここに、JavaScriptの歴史的背景とTypeScriptの型システムが抱える「ちょっとしたモヤモヤ」が隠されています。
—
2. 「プロパティが存在しないこと」と「値が undefined であること」の曖昧さ
JavaScriptの世界では、`{ name: “Charlie”, age: undefined }` というオブジェクトと、`{ name: “Charlie” }` というオブジェクトは、厳密には「別物」です。
- 前者は `age` というキーが「存在して」、その値が `undefined` です。
- 後者は `age` というキー自体が「存在しない」 です。
これらを `Object.keys()` や `for…in`、あるいはJSONシリアライズ(`JSON.stringify`)で扱ってみると、挙動が違うことに気づきます。
console.log(Object.keys({ name: “Alice” }));
// 出力: [ ‘name’ ] (ageキーは影も形もない)
console.log(Object.keys({ name: “Charlie”, age: undefined }));
// 出力: [ ‘name’, ‘age’ ] (ageキーがしっかり居座っている)
APIのペイロードやデータベースの更新処理を書くとき、「キー自体を送らないでほしい(部分更新)」ケースと、「明示的に値をクリア(null/undefinedに)してほしい」ケースを厳密に区別したい場面は多々ありますよね。
しかし、デフォルトのTypeScriptでは、`age?: number` と書くだけで「`undefined` を値として入れること」が許されてしまうため、この境界線が曖昧になってしまっていたのです。
—
3. 救世主:`exactOptionalPropertyTypes` の登場
そこでTypeScript 4.4で導入されたのが、tsconfig.jsonのコンパイラオプションである `exactOptionalPropertyTypes` です。
これを有効(`true`)にすると、TypeScriptのコンパイラはこう宣言します。
> 「`?` で定義されたオプショナルプロパティには、`undefined` を値として代入することを禁止する。代入したいなら、型に `| undefined` を明示しなさい」
設定方法 (`tsconfig.json`)
{
“compilerOptions”: {
“target”: “ESNext”,
“module”: “NodeNext”,
“strict”: true,
“exactOptionalPropertyTypes”: true // ここを true にする!
}
}
—
4. 有効化した世界で何が起きるか?(具体例とエラーの読み解き)
それでは、`exactOptionalPropertyTypes: true` の環境下で、先ほどのコードがどう型評価されるかを見てみましょう。
type StrictUserOptions = {
name: string;
age?: number; // ここは「省略可能」であって「undefinedを代入していい」わけではない
};
// OK: プロパティの省略
const user1: StrictUserOptions = { name: “Alice” };
// OK: 正しい数値の代入
const user2: StrictUserOptions = { name: “Bob”, age: 25 };
// ❌ エラー発生!
// TS2376: Type ‘undefined’ is not assignable to type ‘number’ with ‘exactOptionalPropertyTypes: true’.
const user3: StrictUserOptions = { name: “Charlie”, age: undefined };
見事にコンパイルエラーになりましたね!
「オプショナルにしたいのか、それとも `undefined` を許容する値にしたいのか」を、TypeScriptが厳格に区別させるようになった瞬間です。
じゃあ、`undefined` を許容したいときはどう書くの?
「いや、私はどうしても `undefined` を代入できる余地を残したいんだ!」という場合は、型定義側で明示的に `| undefined` を付与します。
type FlexibleUserOptions = {
name: string;
// 「省略可能」かつ「undefinedを入れてもいい」と明示的に許す
age?: number | undefined;
};
// これなら exactOptionalPropertyTypes が有効でもエラーにならない!
const user4: FlexibleUserOptions = { name: “Dave”, age: undefined };
このように、「キー自体を無くすことを許す(`?`)」のと、「値として `undefined` が入ることを許す(`| undefined`)」が綺麗に分離されるため、コードの意図が圧倒的に明確になります。
—
5. 実務におけるメリットと関数引数での活用
この機能は、オブジェクトの型定義だけでなく、関数のオプショナル引数を設計する際にも強力に働きます。
// 設定オブジェクトを受け取るヘルパー関数
function updateUser(id: string, options?: { role?: string; bio?: string }) {
// 処理…
}
// exactOptionalPropertyTypes が有効な場合:
// bio にうっかり undefined を渡すミスをコンパイル時に防げる
updateUser(“123”, { role: “admin”, bio: undefined }); // ❌ コンパイルエラー!
ライブラリのAPIや、チームで共有する共通モジュールを作る際、「呼び出し側が意図せず `undefined` を混ぜ込んでしまい、下流のロジック(`Object.keys` や `hasOwnProperty` の判定など)でバグる」という事故を、コンパイラが未然に防いでくれるようになります。
—
まとめ:ここをクリアすればTypeScriptはもっと楽しくなる!
- `exactOptionalPropertyTypes: true` を有効にすると、オプショナルプロパティ(`?`)に `undefined` を直接代入できなくなる。
- 「キーが存在しない状態」と「値が `undefined` の状態」を厳密に区別できるため、データの整合性が上がる。
- `undefined` も値として許容したい場合は、`prop?: type | undefined` と明示的に書く。
初学者のうちは「エラーが厳しすぎる!」と感じるかもしれませんが、この厳格さこそが、大規模開発や長期運用でTypeScriptが最強の盾となる理由です。
ここをマスターしたあなたなら、もうTypeScriptの型システムに怯える必要はありません。自信を持って、より堅牢で美しいコードを書いていきましょう!