arktype icon indicating copy to clipboard operation
arktype copied to clipboard

How to properly set up a discriminated union?

Open Scalahansolo opened this issue 8 months ago • 2 comments

I have put together the following...

export const UnionTypeOne = type({
  type: '"1"',
  foo: 'string',
})

export const UnionTypeTwo = type({
  type: '"2"',
  bar: 'number',
})

export const UnionTypeThree = type({
  type: '"3"',
  baz: 'boolean',
})

export const ResponseSchema = type({
  union: UnionTypeOne.or(UnionTypeTwo).or(UnionTypeThree),
})

The thing I'm missing here is that my union key is not properly type checked where I want to essentially set up a union where any of the values in the union have to have a specifier, in this case type. Maybe I'm just missing it in the docs, but I can't figure out how to set this up.

Scalahansolo avatar May 08 '25 21:05 Scalahansolo

Hi @Scalahansolo, I believe the key to discriminated unions is to include the other keys as optional & never, e.g.:

const foo = type({
    type: "'1'",
    foo: "string",
    "bar?": "never",
    "baz?": "never"
});

const bar = type({
    type: "'2'",
    bar: "number",
    "foo?": "never",
    "baz?": "never"
});

const baz = type({
    type: "'3'",
    baz: "boolean",
    "foo?": "never",
    "bar?": "never"
});

const union = foo.or(bar).or(baz);

Give something like this a try and let me know how it goes!

TizzySaurus avatar May 09 '25 13:05 TizzySaurus

That isn't what a discriminated union is. Both Zod and Valibot support these like so.

Valibot

// Discriminated union components
export const UnionTypeOne = v.object({
  type: v.literal('1'),
  foo: v.string(),
})

export const UnionTypeTwo = v.object({
  type: v.literal('2'),
  bar: v.number(),
})

export const UnionTypeThree = v.object({
  type: v.literal('3'),
  baz: v.boolean(),
})

export const ResponseSchema = v.object({
  union: v.variant('type', [UnionTypeOne, UnionTypeTwo, UnionTypeThree]),
})

Zod

export const UnionType = z.enum(['1', '2', '3'])

export const UnionTypeOne = z.object({
  type: UnionType.enum['1'],
  foo: z.string(),
})

export const UnionTypeTwo = z.object({
  type: UnionType.enum['2'],
  bar: z.number(),
})

export const UnionTypeThree = z.object({
  type: UnionType.enum['3'],
  baz: z.boolean(),
})

export const ResponseSchema = z.object({
  union: z.discriminatedUnion('type', [
    UnionTypeOne,
    UnionTypeTwo,
    UnionTypeThree,
  ]),
})

In both of these cases, when establishing the key union, there is type checking happening to ensure that each schema being added to the union has the type key.

Scalahansolo avatar May 09 '25 13:05 Scalahansolo