TypeScriptの「オプショナル引数」と「undefined」の深淵:型システムが隠蔽する実行時の罠
TypeScriptで開発をしていると、誰もが一度は `?` を使ったオプショナル引数にお世話になるはずだ。しかし、その気軽な記述が、実は「コンパイル時の型安全性」と「実行時の挙動」の間に微妙な歪みを生んでいることを意識しているだろうか。
今回は、単なる構文解説ではなく、TypeScriptのコンパイラが型をどう評価し、それが我々の書くコードにどう影響するか、その「深淵」を紐解いていく。
—
1. `?` と `| undefined` の本質的な違い
まず、基本を再確認する。多くのエンジニアが混同しているが、以下の2つはTypeScriptの型システム上、明確に異なる意味を持つ。
// パターンA: オプショナル引数
function greet(name?: string) { … }
// パターンB: 明示的なUnion型
function greet(name: string | undefined) { … }
コンパイル時の差異
- パターンA (`name?: string`): 引数を省略可能にする。呼び出し側は `greet()` と書ける。
- パターンB (`name: string | undefined`): 引数は必須だが、値として `undefined` を許容する。呼び出し側は必ず `greet(undefined)` と書かなければならない。
この違いは、単なる「呼び出し方の違い」に留まらない。「引数が渡されなかった」ことと「undefinedが渡された」ことの区別を、コンパイラがどう扱うかという問題に直結する。
—
2. なぜ `?` を使うべきか、使わざるべきか
実務において、API連携のDTO(Data Transfer Object)やコンポーネントのProps設計では、この区別がバグの温床になる。
`?` を使うべきケース:関数の「振る舞い」を切り替えるとき
関数が内部で「引数があるかないか」を判定し、デフォルト値を用意したり処理をスキップする場合、`?` は非常に強力だ。
`| undefined` を使うべきケース:データの「状態」を厳密に扱うとき
APIから返却されるオブジェクトのプロパティや、Redux/Stateの更新関数など、「値が存在しない」という事実そのものがデータの一部である場合は、必ず `| undefined` を明示するべきだ。
—
3. 実践:バグを生まない「堅牢な設計」パターン
現場でよくある「値が渡されないとクラッシュする」問題を防ぐための、最も保守性の高いパターンを提示する。
おすすめの設計:デフォルト引数との併用
`?` を使う際は、原則としてデフォルト引数をセットで考える。これにより、内部実装での `undefined` チェックの煩雑さを排除できる。
/
- 堅牢な設計パターン:
- 1. オプショナル引数でインターフェースを広げる
- 2. デフォルト引数で実行時の安全性を担保する
/
const fetchUser = (id: string, options: { retries?: number } = {}) => {
// ここで const retries = options.retries ?? 3 とすれば、
// 呼び出し側の引数漏れと undefined の両方を安全に処理できる
const { retries = 3 } = options;
console.log(`Fetching user ${id} with ${retries} retries…`);
};
// 完璧な呼び出し例
fetchUser(“user_123”);
fetchUser(“user_123”, { retries: 5 });
—
4. プロダクションコードにおける「見えない罠」
特に注意が必要なのが、`exactOptionalPropertyTypes` オプションだ。この設定が有効な場合、TypeScriptは `{ a?: number }` に `{ a: undefined }` を代入することを禁止する。
interface Config {
timeout?: number;
}
const config: Config = { timeout: undefined };
// tsconfig で exactOptionalPropertyTypes: true の場合、ここでエラーになる。
// 「プロパティ自体が存在しない」ことと「プロパティはあるが値がundefined」を
// 区別したいシチュエーションが増えているため、近年のベストプラクティスは
// 「オプショナルにはなるべく undefined を代入しない」ことにある。
—
結論:コードレビューで意識すべきこと
技術リードとして、私がコードレビューで常に意識しているのは「その型定義は、未来の変更に耐えられるか」という点だ。
1. 引数を省略可能にしたいだけか? → `?` を使う。
2. 値が存在しない状態を保持したいか? → `| undefined` を使い、`?` は使わない。
3. 引数のデフォルト値は明確か? → `?` を使うなら、関数内部で必ずデフォルト値を定義する。
`?` は便利なシンタックスシュガーだが、多用すると「何が渡される可能性があるのか」が曖昧になる。型定義を厳密に書くことは、単にコンパイラを黙らせるためではない。「実行時、この関数はどのような入力を受け取って動くべきか」という仕様を、コードそのものに刻み込む行為なのだ。
この違いを理解し、適切に使い分けることが、あなたの書くTypeScriptを「動くコード」から「保守可能な資産」へと昇華させる。次にコードを書くとき、その `?` が本当に最適解なのか、一度自問してみてほしい。