tcomb icon indicating copy to clipboard operation
tcomb copied to clipboard

Different errror on union with and without dispatch

Open ArmorDarks opened this issue 8 years ago • 1 comments

Version

3.2.23

Expected behaviour

Errors should be same or at least close

Actual behaviour

This is follow up of https://github.com/gcanti/tcomb/issues/295#issuecomment-329029432.

After using of union of interfaces instead of union of structs with dispatch, turned out that type errors are now different and at some point are less informative.

Steps to reproduce

Taken this code:

const Price = t.union([t.Number, t.interface({
  from: t.Boolean,
  value: t.Number
}, { name: 'PriceObject', strict: true })])

const BaseOffer = t.interface({
  id: t.String,
  price: Price
}, { name: 'Offer', strict: true })

const SimplifiedOffer = BaseOffer.extend({
  name: t.String
}, { name: 'SimplifiedOffer', strict: true })

const FreeOffer = BaseOffer.extend({
  type: t.String,
  model: t.String
}, { name: 'FreeOffer', strict: true })

const Offer = t.union([FreeOffer, SimplifiedOffer], 'Offer')
Offer.dispatch = (x) => x.type
  ? FreeOffer
  : SimplifiedOffer
Offer({ id: 'nope', price: { value: 12 } })

will throw:

[tcomb] Invalid value {
  "value": 12
} supplied to Offer(SimplifiedOffer)/price: Number | PriceObject (no constructor returned by dispatch)"

but if price would be changed to

const PriceObject = t.interface({
  from: t.Boolean,
  value: t.Number
}, { name: 'PriceObject', strict: true })

const Price = t.union([t.Number, PriceObject])

Price.dispatch = (x) => t.Number.is(x)
  ? t.Number
  : PriceObject

it will throw

[tcomb] Invalid value undefined supplied to Offer(SimplifiedOffer)/price: Number | PriceObject(PriceObject)/from: Boolean

Notice here /from part, which indicates clearly which exactly property lacks the value, while in first case it is unclear


Now, relatively same thing happens with other interfaces or structs without dispatch. Here is another example (close to first, but slightly simplified):

const BaseOffer = t.interface({
  id: t.String
}, { name: 'Offer', strict: true })

const SimplifiedOffer = BaseOffer.extend({
  name: t.String
}, { name: 'SimplifiedOffer', strict: true })

const FreeOffer = BaseOffer.extend({
  type: t.String,
  model: t.String
}, { name: 'FreeOffer', strict: true })

const Offer = t.union([FreeOffer, SimplifiedOffer], 'Offer')
Offer.dispatch = (x) => x.type
  ? FreeOffer
  : SimplifiedOffer

Given

 Offer({ id: 'nope' }))

It will throw

[tcomb] Invalid value undefined supplied to Offer(SimplifiedOffer)/name: String

But if dispatch from Offer removed (since interfaces do not require them):

const Offer = t.union([FreeOffer, SimplifiedOffer], 'Offer')

Error message suddenly becomes much more obscure:

[tcomb] Invalid value {
  "id": "nope"
} supplied to Offer (no constructor returned by dispatch)

Note, that it is no longer clear to which interface resolved union, and it no longer clearly says that name property has no value, while should

ArmorDarks avatar Sep 13 '17 13:09 ArmorDarks

This is due to how the default dispatch is defined: it may return undefined while your custom dispatch always returns a constructor

EDIT: also note that defining a custom dispatch may be a good strategy even when using interfaces since can be smarter than the default one: in your first use case you have just two constructors and the first is t.Number

gcanti avatar Sep 13 '17 16:09 gcanti