YASD-TECH
YASD TECH
# TypeScript

判別可能ユニオンとundefinedプロパティ

投稿日:2026/7/26

更新日:2026/7/26

ttitleImage

判別可能ユニオンと ?: undefined

判別可能ユニオン (discriminated union) で「この枝にはこのプロパティが無い」を表すときに出てくる ?: undefined という書き方の意味と、使うべきかどうかの判断基準。


1. 出発点:素朴に書いた判別可能ユニオン

支払い方法を例にする。カード払いならカード番号、銀行振込なら口座番号を持つ。

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 枝に絞られている
  }
};

判別可能ユニオンの基本はこれで十分。 以降の話は「分岐せずに読みたい」場合にだけ必要になる。


2. 分岐せずに読もうとすると失敗する

「カード番号があれば返す、無ければ null」のような関数を、分岐なしで書こうとする。

const getCardNumber = (payment: Payment): string | null => {
  return payment.cardNumber ?? null;
  //             ~~~~~~~~~~
  // ❌ エラー: プロパティ 'cardNumber' は型 'Payment' に存在しません。
  //           型 '{ method: "bank"; accountNumber: string; }' に 'cardNumber' はありません。
};

TypeScript は「union の全枝に存在するプロパティ」しか直接読ませない。 bank 枝に cardNumber が無いので、payment.cardNumber という式そのものが書けない。


3. ?: 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 になる。


4. なぜ ?(オプショナル)が要るのか

ここが分かりにくいところ。? を外すとどうなるか。

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' }; // ✅

2つの記号の役割分担

記号 役割
undefined 全枝にプロパティが存在する形になり、分岐せずに読める
? 値を作るときに書かなくてよくなる

セットで「書かなくていいが、読むことはできる」を作っている。

exactOptionalPropertyTypes との関係

tsconfig.jsonexactOptionalPropertyTypes の有無で ? の意味が変わる。

設定 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 以外取れない)

5. 実際の値には現れない

?: 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 は同じになる。


6. では常に ?: 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;
};

型定義が素直になり、「カード払いだけがカード番号を持つ」ことが一目で分かる。


7. 関連:?: 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 を選ぶ


8. Zod で同じ形を書く

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 に落とす。


まとめ

  1. 判別可能ユニオンは、分岐して読む限り ?: undefined は要らない
  2. ?: undefined は「分岐せずに読みたい」ときの回避策
    • undefined … 全枝にキーが存在する形になり読める
    • ? … 値を作るとき書かなくて済む
  3. 型の上だけの話で、実行時の値・JSON・DB には現れない
  4. 読む側が数箇所なら、そちらを分岐に直す方が型定義が素直になる
  5. exactOptionalPropertyTypes: true では ? だけでは undefined を明示代入できないので、型に undefined を含める必要がある

Index

  • 判別可能ユニオンと ?: undefined
  • 1. 出発点:素朴に書いた判別可能ユニオン
  • 2. 分岐せずに読もうとすると失敗する
  • 3. ?: undefined を足すと読めるようになる
  • 4. なぜ ?(オプショナル)が要るのか
  • 2つの記号の役割分担
  • exactOptionalPropertyTypes との関係
  • 5. 実際の値には現れない
  • 6. では常に ?: undefined を書くべきか → いいえ
  • 判断の目安
  • 分岐に直す例
  • 7. 関連:?: never との違い
  • 8. Zod で同じ形を書く
  • z.discriminatedUnion が使えるなら使う
  • まとめ