superstruct icon indicating copy to clipboard operation
superstruct copied to clipboard

union of intersections doesn't work

Open vsapronov opened this issue 3 years ago • 5 comments

Here're types definitions:

import * as t from './superstruct'

const TOrderCreated = t.object({
    id: t.string(),
    sku: t.string(),
    quantity: t.number(),
})

export const TOrderChanged = t.object({
    id: t.string(),
    quantity: t.number(),
})

const TOrderCanceled = t.object({
    id: t.string(),
})

const TOrderEvent = t.union([
    t.intersection([t.type({_type: t.literal('created')}), TOrderCreated]),
    t.intersection([t.type({_type: t.literal('changed')}), TOrderChanged]),
    t.intersection([t.type({_type: t.literal('canceled')}), TOrderCanceled]),
])

type OrderEvent = t.Infer<typeof TOrderEvent>

Clearly I'm using _type as discriminator field, but I don't want to mix it with the "payload" - specific events. Hence I'm doing intersection for adding discriminator field.

I would expect this to work properly:

let event: OrderEvent = t.create({ _type: 'changed', id: 'id123', quantity: 123 }, TOrderEvent)

However I get error:

StructError: Expected the value to satisfy a union of `intersection | intersection | intersection`, but received: [object Object]

vsapronov avatar Aug 17 '21 19:08 vsapronov

And by the way: in io-ts this is fully working (I'm trying to achieve with superstruct what I already have working in io-ts). The exact same code with only difference that Infer is replaced with TypeOf - I have checked this in my tests.

vsapronov avatar Aug 17 '21 19:08 vsapronov

Actually, I have found the reason of my problems: In the code above TOrderCreated is t.object (no extra fields!). So what I have is

t.intersection([t.type({......}), t.object({......})])

This is impossible because t.object does not allow extra fields and t.intersection is literally adding extra fields. When I replaced t.object with t.type everything works as expected, so this would work:

t.intersection([t.type({_type: t.literal('created')}), t.type({......})])

Th only real issue I see here is that error message that I saw is vague - doesn't help to understand what's wrong.

vsapronov avatar Aug 23 '21 16:08 vsapronov

@vsapronov I've been running into the same problems as you so I just released @birchill/discriminator for this.

It adds a new struct type based on the JSON typedef discriminator type.

Usage in your example:

import * as t from './superstruct'
import { discriminator } from '@birchill/discriminator';

const TOrderCreated = t.object({
    id: t.string(),
    sku: t.string(),
    quantity: t.number(),
})

export const TOrderChanged = t.object({
    id: t.string(),
    quantity: t.number(),
})

const TOrderCanceled = t.object({
    id: t.string(),
})

const TOrderEvent = discriminator('_type', {
    created: TOrderCreated,
    changed: TOrderChanged,
    canceled: TOrderCanceled,
})

type OrderEvent = t.Infer<typeof TOrderEvent>

// Produces
//
// type OrderEvent = {
//     _type: 'created';
//     id: string;
//     sku: string;
//     quantity: number;
// } | {
//     _type: 'changed';
//     id: string;
//     quantity: number;
// } | {
//     _type: 'canceled';
//     id: string;
// }

When it fails to validate, it will tell you the specific field that failed, e.g.

At path: orderEvent.quantity -- Expected a number received "123"

birtles avatar Aug 26 '21 09:08 birtles

@birtles that's awesome! If you were interested in PR'ing I'd be open to it.

ianstormtaylor avatar Sep 08 '21 17:09 ianstormtaylor

@birtles that's awesome! If you were interested in PR'ing I'd be open to it.

Thanks! Sure, it will take a bit of work to get it fully ready but I'll try to get to it in the coming weeks.

birtles avatar Sep 09 '21 01:09 birtles