Currently when we chain together 2 or more converters with the or function it creates an issue where we're unsure which branch we should be converting. The solution is to try both and throw an error at the top-level if both fail. However this causes problems when combining complex objects and trying to find the source of the error.
Don't combine complex objects with or
The "simple" solution is to not combine complex objects using the .or function and instead use switch or taggedUnion however that advice fails when you want an object or a primitive for instance: t.number.or(t.strict({ value: t.number, format: t.string }))
What Error should be generated?
Given the converter declared as: t.number.or(t.strict({ value: t.number, format: t.string })) the following table represents what I'd propose as errors:
| Input |
Error |
false |
At $ expected number | object but received false |
{ value: 'four', format: 'number' } |
At $.value expected number but received "four" |
{ value: 'four' } |
At $.value expected number but received "four", At $.format expected string but was missing |
Generating these errors:
The heuristic to use for generating errors for or chains should be to find the shallowest path of each side and select the side of the or chain with the deeper path the following table illustrates that from above:
| Input |
Number Error |
Strict Error |
false |
path: $, expected: number |
path: $, expected: object |
{ value: 'four', format: 'number' } |
path: $, expected: number |
path $.value, expected: number |
{ value: 'four' } |
path: $, expected: number |
path $.value, expected: number AND path $.format, expected: string |
In case 2 and 3 above we find that errors from the strict branch are at path depth 2 while the errors from the number branch are at path depth 1, therefore we throw the errors from the strict branch as-is.
In case 1 the branches have the same depth, in this case we need to break a tie. We can find all the errors with the same path and actual value and combine their expected values with the | character. If there are no errors matching that criteria (completely disjoint types) we can generate a new error at the top level.
Generalizing to Union of complex types
This approach of measuring error depth can be applied to the union of complex types, although it probably shouldn't. Let's assume the following converter:
t.strict({
type: t.literal('shirt'),
neck: t.number,
chest: t.number
}).or(t.strict({
type: t.literal('pants'),
waist: t.number,
inseam: t.number
});
Given the input { type: 'dress', bust: 35, waist: 27 } our approach above will produce the following error:
At $.type expected "shirt" | "pants" but was "dress"
this is good.
However given the input: { type: 'shirt', waist: 32, inseam: 34 } our approach will produce the error:
At $ expected { type: "shirt", neck: number, chest: number } | { type: "pants", waist: number, inseam: number } but was { type: "shirt", waist: 32, inseam: 34 }
Without changing our object structure the only other heuristics we have to look at are field names and error counts. The shirt branch is producing 2 errors both at depth 2, one for neck and the other for chest, the pants branch is producing 1 error at depth 2 for type. If we were to consider the branch with the least errors the most likely candidate we would report that we expected "pants" but was "shirt" this is particularly odd, on the other hand selecting the candidate with the most errors also feels wrong as it will likely report on cases which are much more wrong in many cases. Considering field names and giving special standing to fields named "type" or "tag" could help to solve this, however it seems as though simply using a taggedUnion converter is a better approach all-around.
Currently when we chain together 2 or more converters with the
orfunction it creates an issue where we're unsure which branch we should be converting. The solution is to try both and throw an error at the top-level if both fail. However this causes problems when combining complex objects and trying to find the source of the error.Don't combine complex objects with or
The "simple" solution is to not combine complex objects using the
.orfunction and instead useswitchortaggedUnionhowever that advice fails when you want an object or a primitive for instance:t.number.or(t.strict({ value: t.number, format: t.string }))What Error should be generated?
Given the converter declared as:
t.number.or(t.strict({ value: t.number, format: t.string }))the following table represents what I'd propose as errors:false{ value: 'four', format: 'number' }{ value: 'four' }Generating these errors:
The heuristic to use for generating errors for or chains should be to find the shallowest path of each side and select the side of the or chain with the deeper path the following table illustrates that from above:
false{ value: 'four', format: 'number' }{ value: 'four' }In case 2 and 3 above we find that errors from the strict branch are at path depth 2 while the errors from the number branch are at path depth 1, therefore we throw the errors from the strict branch as-is.
In case 1 the branches have the same depth, in this case we need to break a tie. We can find all the errors with the same path and actual value and combine their expected values with the
|character. If there are no errors matching that criteria (completely disjoint types) we can generate a new error at the top level.Generalizing to Union of complex types
This approach of measuring error depth can be applied to the union of complex types, although it probably shouldn't. Let's assume the following converter:
Given the input
{ type: 'dress', bust: 35, waist: 27 }our approach above will produce the following error:this is good.
However given the input:
{ type: 'shirt', waist: 32, inseam: 34 }our approach will produce the error:Without changing our object structure the only other heuristics we have to look at are field names and error counts. The shirt branch is producing 2 errors both at depth 2, one for neck and the other for chest, the pants branch is producing 1 error at depth 2 for type. If we were to consider the branch with the least errors the most likely candidate we would report that we expected "pants" but was "shirt" this is particularly odd, on the other hand selecting the candidate with the most errors also feels wrong as it will likely report on cases which are much more wrong in many cases. Considering field names and giving special standing to fields named "type" or "tag" could help to solve this, however it seems as though simply using a taggedUnion converter is a better approach all-around.