Skip to content

How can I allow null as input value of a schema, but not as a valid output? #1088

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

Open
youyoumu opened this issue Mar 18, 2025 · 12 comments
Open
Assignees
Labels
question Further information is requested

Comments

@youyoumu
Copy link

example

const schema = pipe(
    object({
      duration_minutes: nullable(number()),
    }),
    forward(
      partialCheck(
        [["duration_minutes"]],
        ({ duration_minutes }) => {
          if (duration_minutes === null) return false;
          return true;
        },
        "duration_minutes is null"
      ),
      ["duration_minutes"]
    )
  );

  type InputSchema = InferInput<typeof schema>; // { duration_minutes: number | null }

  // i want this to be { duration_minutes: number }
  type OutputSchema = InferOutput<typeof schema>; // { duration_minutes: number | null }

related issue from zod repo: colinhacks/zod#1206

My common use case is when dealing with form libraries like react-hook-form. I want components value like DatePicker or FilePicker to be null initially, but they should not pass the schema.

@youyoumu
Copy link
Author

this is my workaround as for now

 const schema = object({
    duration_minutes: number(),
  });

  type Nullable<T> = { [K in keyof T]: T[K] | null }; // https://typeofnan.dev/making-every-object-property-nullable-in-typescript/
  type InputSchema = Nullable<InferInput<typeof schema>>;
  type OutputSchema = InferOutput<typeof schema>;

@fabian-hiller
Copy link
Owner

fabian-hiller commented Mar 19, 2025

I would simply write:

import * as v from 'valibot';

const Schema = v.pipe(v.nullable(v.number()), v.number());

type Input = v.InferInput<typeof Schema>; // number | null
type Output = v.InferOutput<typeof Schema>; // number

The first schema of the pipeline defines the input type and the last one the output type.

@fabian-hiller fabian-hiller self-assigned this Mar 19, 2025
@fabian-hiller fabian-hiller added the question Further information is requested label Mar 19, 2025
@lukasvice
Copy link

lukasvice commented Mar 19, 2025

I'm currently looking for the same thing and experimenting with Valibot.

I like the v.pipe syntax you suggested for simple types. But I see a disadvantage in that you have to repeat the type. This can get verbose if you want to do this with objects. You can extract the nested object schema to a variable, but if you have multiple objects or nested objects this can get very hard to read.

The goal is to allow null as input type for address and bestFriend:

const person = v.object({
  name: v.string(),
  address: v.pipe(
    v.nullable(
      v.object({
        street: v.string(),
        country: v.string(),
      }),
    ),
    v.object({
      street: v.string(),
      country: v.string(),
    }),
  ),
  bestFriend: v.pipe(
    v.nullable(
      v.object({
        name: v.string(),
        age: v.number(),
      }),
    ),
    v.object({
      name: v.string(),
      age: v.number(),
    }),
  ),
});

I could do

const address = v.object({
  street: v.string(),
  country: v.string(),
});

const bestFriend = v.object({
  name: v.string(),
  age: v.number(),
});

const person = v.object({
  name: v.string(),
  address: v.pipe(v.nullable(address), address),
  bestFriend: v.pipe(v.nullable(bestFriend), bestFriend),
});

But now, if I want age of bestFriend to also accept null as input, I can't do it this way because I get a TS error in the v.pipe of person.bestFriend: (Playground demo)

const bestFriend = v.object({
  name: v.string(),
  age: v.pipe(v.nullable(v.number()), v.number()),
});

const person = v.object({
  bestFriend: v.pipe(v.nullable(bestFriend), bestFriend),
});

This means I have to repeat the whole object:

const person = v.object({
  bestFriend: v.pipe(
    v.nullable(
      v.object({
        name: v.string(),
        age: v.nullable(v.number()),
      }),
    ),
    v.object({
      name: v.string(),
      age: v.number(),
    }),
  ),
});

Again, this is not a problem with simple and small objects. However, it can become a problem with more complex objects.

What do you think?

@fabian-hiller
Copy link
Owner

This is fixable. I will share some code later.

@fabian-hiller
Copy link
Owner

You have two options. The first is to use nullable with a default value. This way null is a valid input, but defaults to a valid output that is not null. See this playground.

import * as v from 'valibot';

const Schema = v.nullable(v.object({ key: v.string() }), { key: '' });

type Input = v.InferInput<typeof Schema>; // { key: string } | null
type Output = v.InferOutput<typeof Schema>; // { key: string }

The second option is to keep your current code, but write a custom function to avoid repeating yourself. See this playground.

import * as v from 'valibot';

function nullableInput<TSchema extends v.GenericSchema>(schema: TSchema) {
  return v.pipe(v.nullable(schema), schema);
}

const Schema = nullableInput(v.object({ key: v.string() }));

type Input = v.InferInput<typeof Schema>; // { key: string } | null
type Output = v.InferOutput<typeof Schema>; // { key: string }

@lukasvice
Copy link

Thank you very much for your answer!

While the first approach doesn't work for form validation because I want to get an error if the input is null, the second approach is very nice! I didn't think it was possible because if I type everything instead of using the nullableInput function, I get a type error. See this playground.

@fabian-hiller
Copy link
Owner

This TS error is strange. If more people encounter this problem, I will investigate it.

@elmehdielhamdi
Copy link

Hello!
Is there a way I could extend valibot so that I could call nullableInput in the following way v.nullableInput ?
I've looked for it in the docs but I didn't manage to find a reference to this
Thank you!

@fabian-hiller
Copy link
Owner

fabian-hiller commented Apr 27, 2025

@lukasvice I know the reason for the TS error. The first schema will never output null for the keys. So you should remove nullable from the second schema of the pipe. The input type of subsequent schemas must be the same as or a subset of the previous output type.

@fabian-hiller
Copy link
Owner

@elmehdielhamdi is the only difference the v. at the beginning? Why do you prefer to writ it this way?

@lukasvice
Copy link

@fabian-hiller I see, that makes sense. Still, it's strange that it works with the nullableInput function. It probably has something to do with the generic type of the function. Here is a reduced playground.

@fabian-hiller
Copy link
Owner

I know why but we will probably keep this implementation for now. The implementation is safe and does not cause any runtime errors and in most cases this TS error is helpful to write more efficient schemas.

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

No branches or pull requests

4 participants