External fields (for objects and tuples)
This is a feature request. I have some code that looks like this:
import * as v from '@badrap/valita';
// setup
const externalSymbol = Symbol('external') as any;
const isExternal = (v: unknown) => v === externalSymbol;
const external = <T>(t: v.Type<T>): v.Type<T> =>
t.default(externalSymbol)
.assert(isExternal, 'field must be empty');
// child type includes fields that are constructed by another parser
// so they must not get values from the input, but must be expected on the type
type Child = v.Infer<typeof Child>;
const Child = v.object({
name: external(v.string()),
color: v.string(),
});
// parent includes the mapping that assigns children their name
type Parent = v.Infer<typeof Parent>
const Parent = v.object({children: v.record(Child)})
.map(parent => {
const children = Object.entries(parent.children);
for (const [childname, child] of children) {
child.name = childname;
}
return parent;
});
// results
// after parsing, example's children know their own names
const okExample = Parent.parse({
children: {
john: {color: 'mauve'},
frank: {color: 'red'},
}
});
console.log(okExample.children.john.name);
// will not parse, child defines external field
const badExample = Parent.parse({
children: {
john: {color: 'mauve', name: 'lucas'},
frank: {color: 'red'},
}
});
Parent is responsible for completing the construction of Child (because it has the necessary information). To facilitate this Child provides type information for the field, but also marks it as external, this way if the input tries to provide a value it will result in a failure to parse.
The feature request is to add the type modifier .external() to types to express this use case as follows:
type Child = v.Infer<typeof Child>;
const Child = v.object({
name: v.string().external(),
color: v.string(),
});
Bonus points if forgetting to assign them creates a parse error later!
I think this can be achieved with Valita's builtin features by using the strict parsing mode and inferring the Parent and Child types from the final parser:
const Parent = v.object({
children: v
.record(
v.object({
color: v.string(),
})
)
.map((children) =>
Object.fromEntries(
Object.entries(children).map(([name, child]) => [
name,
{ name, ...child },
])
)
),
});
type Parent = v.Infer<typeof Parent>;
type Child = Parent["children"][string];
const example = Parent.parse(someInput, { mode: "strict" });
The strict mode makes Valita fail parsing when it encounters an unexpected object key:
Parent.parse(
{
children: { alice: { color: "blue", name: "bob" } },
},
{ mode: "strict" }
);
// ValitaError: unrecognized_key at .children.alice (unrecognized key "name")
In fact, since Valita v0.0.21 released yesterday, the strict mode is the default (the old behavior can be enabled by setting { mode: "passthrough" }):
Parent.parse({
children: { alice: { color: "blue", name: "bob" } },
});
// ValitaError: unrecognized_key at .children.alice (unrecognized key "name")
It's also possible for v.object to "opt-in" to strict parsing regardless of the parsing mode:
// fail parsing if "name" is present.
v.object({
color: v.string(),
name: v.never().optional()
})
or
// fail parsing if "color" is not the only key present
v.object({
color: v.string(),
})
.rest(v.never())
I'm assuming this was solved, so closing this issue 👍