Zodのz.discriminatedUnionは結局、識別プロパティの指定が必要な仕様に戻ってた

以前にZod v4リリース前のz.discriminatedUnionを見てた。このときはまだv4リリース前のベータ版だった。

そういえば、ドキュメント修正のIssueあげたことをすっかり忘れてたなぁ。どうなったんだろう?って思って見てみたら、最終的に仕様が変わってた。あら。

No longer applicable now that z.discriminatedUnion() now requires you to specify a discriminator (as before): #4402 This was one of the issues that led me to refactor the implementation. Thanks for the report.

こういう感じかな:

z.discriminatedUnion() では(以前と同様に)識別プロパティの指定が必要になったので対応は不要になりました: #4402 この部分は実装をリファクタリングすることになった理由の一つです。ご報告ありがとうございます。

じゃ、動作確認してみるか

Zod 4.1.13 で確認

識別プロパティは必須に戻ってた

識別プロパティを指定していないとコンパイルエラーになった。つまり、v3までと同じ感覚で使えばよさそうね。この方が挙動が分かりやすくていいよね。

import { z } from 'zod';

// 最初の引数の 'kind' の部分は必須。指定してないとコンパイルエラーになる。
const MyUnion = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('square'),
    size: z.number(),
  }),
  z.object({
    kind: z.literal('rectangle'),
    width: z.number(),
    height: z.number(),
  }),
]);

const result = MyUnion.safeParse({
  kind: 'rectangle',
  width: 5,
  height: 10,
});

console.log(result.data);

Issueはこれ。

識別プロパティがないと問題になるケースがあったから、結局必須に戻したと。識別プロパティを指定することになって、パフォーマンスもv4ベータ版より良くなったみたいね。

識別プロパティがスキーマに存在しない場合

は、どうなるんだろう?って思ったのでやってみた。

たとえば、rectangleの方のkindをコメントアウトしてみても、コンパイル時にはエラーにならなくて、実行時にエラーになる。なるほど。

import { z } from 'zod';

const MyUnion = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('square'),
    size: z.number(),
  }),
  z.object({
    // kind: z.literal('rectangle'),
    width: z.number(),
    height: z.number(),
  }),
]);

const result = MyUnion.safeParse({
  kind: 'rectangle',
  width: 5,
  height: 10,
});

console.log(result.data);

実行したらこういう感じのエラーが出る

Error: Invalid discriminated union option at index "1"

こういうケースってスキーマの記述ミスだから、UT書いて捕まえとけばいいかな。

他のスキーマと組み合わせられるのはそのまま

組み合わせるのは大丈夫なまま。よき。ベータ版のときと違って、識別プロパティは指定しないといけないけどね。スキーマが識別プロパティを持っていない場合は実行時エラーになるのでそこだけ注意かな。

import { z } from 'zod';

const MyUnion = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('square'),
    size: z.number(),
  }),
  z.object({
    kind: z.literal('rectangle'),
    width: z.number(),
    height: z.number(),
  }),
]);

const MyUnion2 = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('circle'),
    radius: z.number(),
  }),
  MyUnion,
]);

const result1 = MyUnion2.safeParse({
  kind: 'rectangle',
  width: 5,
  height: 10,
});
console.log(result1.data);

const result2 = MyUnion2.safeParse({
  kind: 'circle',
  radius: 5,
});

console.log(result2.data);

optionsは取得できるように戻ってた

ベータ版のときはoptionsがundefinedを返すようになってたけど、今はまた使えるように戻っているみたい。よかったね。

import { z } from 'zod';

const MyUnion = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('square'),
    size: z.number(),
  }),
  z.object({
    kind: z.literal('rectangle'),
    width: z.number(),
    height: z.number(),
  }),
]);

console.log(MyUnion.options.map((option) => option.shape.kind.value));

このコードを実行するとスキーマの要素にアクセスできて、こういう結果になる。

[ 'square', 'rectangle' ]

結局?

  • v3からv4で内部実装が改善された
  • 識別プロパティが必要なことはv3から変わらない
  • optionsでスキーマの要素にアクセスできることもv3から変わらない
  • スキーマを組み合わせられるのはv4の新機能

って感じかな。よさそう。

余談:どうして突然Zodの記事を思い出したのか?

カケハシの同僚の @kosui_me が会社のテックブログで参照記事として取り上げてくれてたからでした。ありがとうございます。

kosuiさんのTSの記事、勉強になるから好き。みんなもぜひ見てみてねー!