投稿日:2026/7/26
更新日:2026/7/26

?: undefined判別可能ユニオン (discriminated union) で「この枝にはこのプロパティが無い」を表すときに出てくる ?: undefined という書き方の意味と、使うべきかどうかの判断基準。
支払い方法を例にする。カード払いならカード番号、銀行振込なら口座番号を持つ。
type Payment =
| { method: 'card'; cardNumber: string }
| { method: 'bank'; accountNumber: string };
method で分岐すれば、それぞれのプロパティに安全にアクセスできる。
const describe = (payment: Payment): string => {
switch (payment.method) {
case 'card':
return `カード ${payment.cardNumber}`; // ✅ card 枝に絞られている
case 'bank':
return `口座 ${payment.accountNumber}`; // ✅ bank 枝に絞られている
}
};
判別可能ユニオンの基本はこれで十分。 以降の話は「分岐せずに読みたい」場合にだけ必要になる。
「カード番号があれば返す、無ければ null」のような関数を、分岐なしで書こうとする。
const getCardNumber = (payment: Payment): string | null => {
return payment.cardNumber ?? null;
// ~~~~~~~~~~
// ❌ エラー: プロパティ 'cardNumber' は型 'Payment' に存在しません。
// 型 '{ method: "bank"; accountNumber: string; }' に 'cardNumber' はありません。
};
TypeScript は「union の全枝に存在するプロパティ」しか直接読ませない。 bank 枝に cardNumber が無いので、payment.cardNumber という式そのものが書けない。
?: undefined を足すと読めるようになる無い方の枝にも「値が undefined のプロパティ」として宣言しておく。
type Payment =
| { method: 'card'; cardNumber: string; accountNumber?: undefined }
| { method: 'bank'; accountNumber: string; cardNumber?: undefined };
これで全枝に cardNumber が存在することになり、分岐なしで読める。
const getCardNumber = (payment: Payment): string | null => {
return payment.cardNumber ?? null; // ✅ 通る
};
payment.cardNumber の型は string | undefined になる。
?(オプショナル)が要るのかここが分かりにくいところ。? を外すとどうなるか。
type Payment =
| { method: 'card'; cardNumber: string; accountNumber: undefined } // ? なし
| { method: 'bank'; accountNumber: string; cardNumber: undefined };
読む側は同じように書けるが、値を作る側が壊れる。
const payment: Payment = { method: 'card', cardNumber: '1234' };
// ❌ エラー: プロパティ 'accountNumber' がありません。
? が無いと「キーが必須で、値が undefined」という意味になるため、毎回こう書かされる。
const payment: Payment = {
method: 'card',
cardNumber: '1234',
accountNumber: undefined, // 😩 毎回これを書く
};
? を付ければ省略できる。
type Payment =
| { method: 'card'; cardNumber: string; accountNumber?: undefined } // ? あり
| { method: 'bank'; accountNumber: string; cardNumber?: undefined };
const payment: Payment = { method: 'card', cardNumber: '1234' }; // ✅
| 記号 | 役割 |
|---|---|
undefined |
全枝にプロパティが存在する形になり、分岐せずに読める |
? |
値を作るときに書かなくてよくなる |
セットで「書かなくていいが、読むことはできる」を作っている。
exactOptionalPropertyTypes との関係tsconfig.json の exactOptionalPropertyTypes の有無で ? の意味が変わる。
| 設定 | foo?: string の意味 |
|---|---|
false(既定) |
キー省略 OK、foo: undefined の明示代入も OK |
true |
キー省略 OK、foo: undefined の明示代入は NG |
true の下で「キー省略も undefined 代入も許したい」なら、型に undefined を明示的に含める必要がある。
foo?: string; // キー省略のみ OK
foo?: string | undefined; // キー省略 + undefined 代入の両方 OK
accountNumber?: undefined; // キー省略 + undefined 代入の両方 OK(値は undefined 以外取れない)
?: undefined は型の上でだけ存在する宣言で、実行時の値には一切影響しない。
const payment: Payment = { method: 'card', cardNumber: '1234' };
console.log(payment);
// { method: 'card', cardNumber: '1234' }
// ↑ accountNumber というキーは存在しない
console.log('accountNumber' in payment); // false
console.log(JSON.stringify(payment)); // {"method":"card","cardNumber":"1234"}
API を流れる JSON にも DB にも現れない。 undefined が入るのですらなく、キーごと存在しない。
JSON.stringifyは値がundefinedのキーも落とすので、仮にaccountNumber: undefinedと明示的に書いたとしても、送信される JSON は同じになる。
?: undefined を書くべきか → いいえ枝が増えるほど宣言が増えるという代償がある。3枝×3プロパティなら最大6行の ?: undefined が生える。
type Payment =
| { method: 'card'; cardNumber: string; accountNumber?: undefined; codFee?: undefined }
| { method: 'bank'; accountNumber: string; cardNumber?: undefined; codFee?: undefined }
| { method: 'cod'; codFee: number; cardNumber?: undefined; accountNumber?: undefined };
// 😩 本質的な情報より undefined の方が多い
しかも ?: undefined を書いた瞬間、「この枝にはこのプロパティが無い」という設計意図が読み取りにくくなる。
| 状況 | 選択 |
|---|---|
| 読む側を分岐で絞れる | ?: undefined を書かない(型定義を素直に保つ) |
| 分岐せず読む箇所が多数に散っている | ?: undefined を検討 |
| 分岐せず読む箇所が 1〜数箇所 | その箇所を分岐に直す方が良い(型定義は全利用者に影響するため) |
型定義は全利用者が読むが、読む側の分岐は1箇所で済む。 コストを払う場所としては、読む側に寄せた方が総量が小さくなることが多い。
3 の getCardNumber は、こう書けば ?: undefined 無しで済む。
type Payment =
| { method: 'card'; cardNumber: string }
| { method: 'bank'; accountNumber: string };
const getCardNumber = (payment: Payment): string | null => {
return payment.method === 'card' ? payment.cardNumber : null;
};
型定義が素直になり、「カード払いだけがカード番号を持つ」ことが一目で分かる。
?: never との違い「このプロパティは絶対に持てない」を強く表したい場合に never を使うことがある。
type Payment =
| { method: 'card'; cardNumber: string; accountNumber?: never }
| { method: 'bank'; accountNumber: string; cardNumber?: never };
| 書き方 | 読めるか | undefined を明示代入 |
|---|---|---|
accountNumber?: undefined |
✅ 型は string | undefined |
✅ できる |
accountNumber?: never |
✅ 型は string | undefined |
❌ できない |
never の方が厳しいが、読む側の体験はほぼ同じで、エラーメッセージが分かりにくくなりやすい。通常は ?: undefined を選ぶ。
const paymentSchema = z.union([
z.object({
method: z.literal('card'),
cardNumber: z.string(),
accountNumber: z.undefined().optional(),
}),
z.object({
method: z.literal('bank'),
accountNumber: z.string(),
cardNumber: z.undefined().optional(),
}),
]);
| Zod | 意味 |
|---|---|
z.undefined() |
値が undefined であること |
.optional() |
キーが無くてもよい |
型側の ?: undefined と対応している。z.undefined() だけだとキーが必須になり、.optional() だけだと任意の型を受け入れてしまう。
z.discriminatedUnion が使えるなら使うz.union は全枝を順に試すためエラーメッセージが分かりにくい。判別キーがあるなら z.discriminatedUnion の方が、エラーが「どの枝の何が不正か」まで絞られる。
const paymentSchema = z.discriminatedUnion('method', [
z.object({ method: z.literal('card'), cardNumber: z.string() }),
z.object({ method: z.literal('bank'), accountNumber: z.string() }),
]);
ただし同じ判別値で複数の枝を作れない制約がある(例:method: 'card' の枝を2つ持てない)。その場合は z.union に落とす。
?: undefined は要らない?: undefined は「分岐せずに読みたい」ときの回避策undefined … 全枝にキーが存在する形になり読める? … 値を作るとき書かなくて済むexactOptionalPropertyTypes: true では ? だけでは undefined を明示代入できないので、型に undefined を含める必要がある