valita icon indicating copy to clipboard operation
valita copied to clipboard

Input & output types

Open Shuunen opened this issue 3 years ago • 10 comments

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

Shuunen avatar Nov 16 '22 17:11 Shuunen

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>

jordan-boyer avatar Nov 16 '22 17:11 jordan-boyer

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.

jviide avatar Nov 16 '22 19:11 jviide

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

jordan-boyer avatar Nov 17 '22 07:11 jordan-boyer

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'>;

jordan-boyer avatar Dec 05 '22 10:12 jordan-boyer

Hello. Unfortunately I haven't had the chance to spend time on this issue. However, I'd like to quickly point out some things:

  1. Similarly to Zod, v.string().default("john") is functionally equivalent to v.string().optional().default("john"). Conversely, v.string().default("john").optional() is essentially the same as v.string().optional().

  2. 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.

  3. Regarding your last example: As pointed out above v.string().default("john").optional() is essentially equal to v.string().optional(). So running personSchemaInput.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">;
    

jviide avatar Dec 05 '22 13:12 jviide

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.

dimatakoy avatar Jul 28 '23 16:07 dimatakoy

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"

ArnaudBarre avatar Dec 23 '23 11:12 ArnaudBarre

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.

Geordi7 avatar Feb 14 '24 15:02 Geordi7

Can the map return validation errors?

ArnaudBarre avatar Feb 14 '24 16:02 ArnaudBarre

@ArnaudBarre .map() can't return validation errors, but .chain() can:

v.unknown().chain((x) => x === 1 ? v.ok(x) : v.err("bad value"));

jviide avatar Feb 26 '24 12:02 jviide