TypeScriptの「?」と「undefined」の深淵:関数引数の厳密な挙動を掌握する
こんにちは。TypeScriptのコードベースを日々深く潜り、コンパイラがどう型を計算しているかに思いを馳せるアーキテクトです。
TypeScriptを学んでいると、関数の引数定義で必ずと言っていいほど直面するのが「オプショナル引数(`?`)」と「明示的な `undefined`」の違いです。
一見、「どちらも値がない状態」のように見えますよね? しかし、コンパイラの視点から見ると、これらは全く別物として扱われています。ここを混同したままコードを書くと、後々手痛いバグに直面することになります。
今回は、TypeScriptの型システムの根幹を成すこの「わずかな差異」について、現場の知見を交えて深く掘り下げていきましょう。
—
1. オプショナル引数(`?`)の正体:呼び出し側の「省略権」
まず、`?` を付けたオプショナル引数の定義から見てみましょう。
function greet(name?: string) {
console.log(`Hello, ${name ?? “Guest”}`);
}
// どちらも合法です
greet(“Alice”); // Hello, Alice
greet(); // Hello, Guest
ここで重要なのは、`?` は「引数を省略する権利」を呼び出し側に与える記号だということです。
コンパイル時に、TypeScriptは「この引数が渡されない可能性がある」ことを型として認識します。そのため、内部では `name` は実質的に `string | undefined` として扱われます。
—
2. 明示的な `undefined` の意味:値として存在を認める
一方で、`?` を使わずに引数を定義し、`undefined` を明示的に渡すケースを考えてみます。
function greetStrict(name: string | undefined) {
console.log(`Hello, ${name ?? “Guest”}`);
}
// greetStrict(); // エラー:引数が足りません
greetStrict(undefined); // OK:明示的に undefined を渡すことは許可される
ここが最大のポイントです。`?` を使わない場合、呼び出し側は必ず引数を「提供」しなければなりません。 たとえその中身が `undefined` であったとしても、関数は「引数がある(値が `undefined` である)」という事実を要求します。
—
3. なぜこの違いが重要なのか?(コンパイラの視点)
現場でよくある失敗は、この違いを無視して「単に値がないだけだから一緒でしょ」と安易に考えてしまうことです。しかし、TypeScriptの型チェック(`strictNullChecks: true`)は非常に厳格です。
陥りやすい罠:関数のオーバーロードやライブラリ開発
例えば、あなたが共通関数を書いているとします。
// パターンA:オプショナル引数
function process(value?: string) {
// ここでの value は string | undefined
}
// パターンB:ユニオン型
function processStrict(value: string | undefined) {
// ここでの value も string | undefined
}
両者の型は一見同じですが、関数の「型シグネチャ」が異なります。
もし、この関数をコールバックとして渡す場合、以下のような挙動の違いが生まれます。
type Callback = (val: string | undefined) => void;
const fn1: Callback = (value?: string) => {}; // OK
const fn2: Callback = (value: string | undefined) => {}; // OK
逆に、引数の数を厳密にチェックする高階関数などの場合、`?` の有無でアサインメントエラー(型代入の失敗)が起きることがあります。「引数が省略可能であること」と「引数が `undefined` を許容すること」は、型システム上ではレイヤーが違うのです。
—
4. 実践的な使い分け指針
現場で設計を行う際は、以下のルールを守るのが「TypeScriptの作法」です。
- 「省略可能(省略しても良い)」な場合 → `?` を使う
- APIのレスポンスや、設定オプションなどで「値を渡さなくてもデフォルト値で動く」場合はこれです。
- 「値は必須だが、データとして欠損があり得る」場合 → `| undefined` を使う
- 例えば、データベースから取得したフィールドが「値がないこともある」という場合は、`?` ではなく明示的に `undefined` を許可するべきです。
現場で役立つTips:`exactOptionalPropertyTypes`
TypeScript 4.4以降、`tsconfig.json` で `exactOptionalPropertyTypes` を有効にすると、`?` で定義されたプロパティに明示的に `undefined` を代入することが禁止されます。
{
“compilerOptions”: {
“exactOptionalPropertyTypes”: true
}
}
これにより、「省略」と「値の欠損」をより厳格に分離でき、より安全なコードが書けるようになります。大規模開発では必須の設定と言えるでしょう。
—
最後に:TypeScriptを「掌握」するために
「`?` を付ければ動く」という段階から、「なぜここで `?` を使うのか、あるいはなぜ `| undefined` を使うべきなのか」という意図を型に込める段階へ進むと、あなたのコードは劇的に堅牢になります。
TypeScriptの型システムは、ただの「エラー防止器」ではありません。あなたの設計思想をコンパイラに伝えるための「言語」です。その一文字一文字に、あなたの意図を込めてみてください。
ここをクリアしたあなたは、もうTypeScriptの基本のその先を見通せていますよ。これからも、型という名の相棒と共に、美しいコードを書いていきましょう!