Input & output types
Hi there,
We really wanted to use your lib over Zod (for performance reasons), but this is the only feature that we miss here on Valita :
const stringToNumber = z.string().transform((val) => val.length);
type input = z.input<typeof stringToNumber>; // string
type output = z.output<typeof stringToNumber>; // number
Is there any way to achieve this with Valita or is this feature planned ?
Regards
For achieving the same result between valita and zod
// Valita
const personSchemaInput = v.object({
name: v.string().default('john').optional(),
age: v.number(),
});
const personSchemaOutput = v.object({
name: v.string(),
age: v.number(),
});
type PersonTypeInput = v.Infer<typeof personSchemaInput>;
type PersonTypeOutput = v.Infer<typeof personSchemaOutput>;
// Zod
const personSchema = z.object({
name: z.string().default('john'),
age: z.number(),
})
type PersonTypeOutput = z.output<typeof personSchema>;
type PersonTypeInput = z.input<typeof personSchema>
Interesting. Can you elaborate on your use-case for the described functionality?
In our work the use-case for Valita has been validating (& parsing) data from external sources, like user input or 3rd party APIs, where we can't safely make assumptions about the structure of the data beforehand. Thus we have always assumed input to be unknown.
Yes ofc.
In our app we use service to call our backend, and we have multiple interface to create some types of object. So our service provide some usefull defaults, but with valitta we have to write two schema to achieve good typing for developpers
Here another example of why we need to have two specific type.
I'll take the same example as above
// good input wrong output
const personSchemaInput = v.object({
name: v.string().default('john').optional(),
age: v.number(),
});
type PersonTypeInput = v.Infer<typeof personSchemaInput>;
// type PersonTypeInput = {
// name?: string | undefined;
// age: number;
// }
// wrong input good output
const personSchemaInput = v.object({
name: v.string().optional().default('john'),
age: v.number(),
});
type PersonTypeInput = v.Infer<typeof personSchemaInput>;
// type PersonTypeInput = {
// name: string;
// age: number;
// }
I've found a solution without creating two object here my solution
type Simplify<T> = {[KeyType in keyof T]: T[KeyType]};
type SetRequired<T, K extends keyof T> = Simplify<PersonTypeInput & Required<Pick<T, K>>>;
const personSchemaInput = v.object({
name: v.string().default('john').optional(),
age: v.number(),
});
type PersonTypeInput = v.Infer<typeof personSchemaInput>;
type PersonTypeOutput = SetRequired<v.Infer<typeof personSchemaInput>, 'name'>;
Hello. Unfortunately I haven't had the chance to spend time on this issue. However, I'd like to quickly point out some things:
-
Similarly to Zod,
v.string().default("john")is functionally equivalent tov.string().optional().default("john"). Conversely,v.string().default("john").optional()is essentially the same asv.string().optional(). -
Regarding your first example: You don't really need to use Valita to define the input type if you just want to use the inferred TypeScript type. You can define the input type directly:
const personSchema = v.object({ name: v.string().default("john"), age: v.number(), }); type PersonTypeOutput = v.Infer<typeof personSchema>; type PersonTypeInput = { name?: string, age: number };Naturally, this approach requires more work than in Zod, but it also requires less work than defining a Valita object and inferring the input type from it.
-
Regarding your last example: As pointed out above
v.string().default("john").optional()is essentially equal tov.string().optional(). So runningpersonSchemaInput.parse({ age: 42 })will have the result{ age: 42 }, which I'm guessing is not the expected output. I would recommend using the explicit type definition method outlined above. You could also create a helper for making certain keys optional:type WithOptionals<T, Keys extends keyof T> = Omit<T, Keys> & Partial<T>; const personSchema = v.object({ name: v.string().default("john"), age: v.number(), country: v.string().default("AUS"), }); type PersonTypeOutput = v.Infer<typeof personSchema>; type PersonTypeInput = WithOptionals<PersonTypeOutput, "name" | "country">;
In my cases, input/output schemas usually differs much more than just defaults or adding .optionals. I really think manually defining two schemas is much more smart(for extending during project lifetime) & common solution.
My usecase for this is using the input type as the type of my form so that:
- I don't have to defined two schemas that are very close and should be kept in sync
- I can think of my schema as a way to transform how the user enters the data to a way my backend wants to process the data
This can be considered out of scope and I will continue using zod, but validation schema are also useful to transform almost know data structure and not always "unkown json"
It seems to me that this is supported out of the box:
const InputType = v.object({
name: v.string().optional(),
age: v.number(),
});
const OutputType = InputType.map(v => ({name: 'john doe', ...v}));
type InputType = v.Infer<typeof InputType>;
type OutputType = v.Infer<typeof OutputType>;
You just have to apply the relevant transforms to the input type to produce the output types.
Can the map return validation errors?
@ArnaudBarre .map() can't return validation errors, but .chain() can:
v.unknown().chain((x) => x === 1 ? v.ok(x) : v.err("bad value"));