Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input & output types #29

Open
Shuunen opened this issue Nov 16, 2022 · 10 comments
Open

Input & output types #29

Shuunen opened this issue Nov 16, 2022 · 10 comments

Comments

@Shuunen
Copy link

Shuunen commented Nov 16, 2022

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

@jordan-boyer
Copy link

jordan-boyer commented Nov 16, 2022

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>

@jviide
Copy link
Contributor

jviide commented Nov 16, 2022

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.

@jordan-boyer
Copy link

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
Copy link

jordan-boyer commented Dec 5, 2022

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

@jviide
Copy link
Contributor

jviide commented Dec 5, 2022

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

@dimatakoy
Copy link
Contributor

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.

@ArnaudBarre
Copy link

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"

@Geordi7
Copy link

Geordi7 commented Feb 14, 2024

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.

@ArnaudBarre
Copy link

Can the map return validation errors?

@jviide
Copy link
Contributor

jviide commented Feb 26, 2024

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants