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

Method of modifying input type to account for acceptable invalid inputs #433

Closed
dboune opened this issue Feb 11, 2024 · 6 comments
Closed
Assignees
Labels
enhancement New feature or request

Comments

@dboune
Copy link

dboune commented Feb 11, 2024

This is a feature request to provide a method of modifying the input type of a schema to account for acceptable invalid inputs.

The use case I am aware of is when composing forms using controlled inputs such as React + MUI or MUI Joy where specific values like null and literal('') (among others) have special meaning as empty value. Having that enshrined in the shape of the form state is helpful to ensure that the state is initialized correctly and allows the developer to handle those cases directly when orchestrating form logic.

In some ways, this is a usage-specific edge case. However, this case is very common, and the solution appears inexpensive.

One drawback is adding an otherwise useless pass-through function that will be compiled when used. In my opinion, the benefits for DX and quality outcomes outweigh the drawbacks.

The following is an API proposed by Fabian in a discussion. (Please note that this is an ultra-simplified example to make it easy to reason about. Objects and deeply nested structures cannot have simple type adjustments made to individual properties without a mechanism like this)

import * as v from 'valibot';

const RepeatSchema = v.input<null>(v.picklist(['daily', 'weekly', 'monthly']));

type RepeatInput = v.Input<typeof RepeatSchema>; // "daily" | "weekly" | "monthly" | null
type RepeatOutput = v.Output<typeof RepeatSchema>; // "daily" | "weekly" | "monthly"

Note: Fabian suggested a potential for a similar output method. I do not immediately recognize the need for it. The Output type provided by Valibot through transformations seems adequate. I would caution against modifying a type away from verified truth after a validation. Inputs, on the other hand, are expected to be potentially invalid.

Alternatives include value and onChange manipulation to modify values back to undefined, but that does require a lot more effort, which, in my opinion, attempts to hide the reality of those components behind an error-prone layer of transformations and makes it difficult or impossible for those using rigorous linter configurations.

The input method provides a clean and easy-to-use API that surfaces acceptable invalid inputs, making them plain to see during development and easy to address, and assists in avoiding additional unnecessary effort.

@fabian-hiller fabian-hiller self-assigned this Feb 11, 2024
@fabian-hiller fabian-hiller added documentation Improvements or additions to documentation enhancement New feature or request and removed documentation Improvements or additions to documentation labels Feb 11, 2024
@fabian-hiller
Copy link
Owner

Thank you for creating this issue! This issue is related to discussion #376.

Do you have an alternative name idea for input?

@dboune
Copy link
Author

dboune commented Feb 11, 2024

input tracks with both Input and at least the motivation for one use case, and here are some other options.

input
inputType
type_
accept
acceptInput
acceptType
allow
allowInput
allowType

@fabian-hiller
Copy link
Owner

I tried to implement input, but there is a problem. input requires two generics to be perfectly typed. One generic for the additional input type and another for the schema. The problem is that TypeScript does not allow us to specify only one generic when calling input. TypeScript requires both generics in this case, which makes the API very complicated to use.

@dboune
Copy link
Author

dboune commented Feb 11, 2024

Ahh, yes, I see the issue. I'm not a type expert by any measure, but I think this has to do with the lack of "partial type argument inference. " Either we infer all type arguments, or we infer none.

The only path I can see, other than individually custom functions, is to use a curried function. For example:

import type { BaseSchema, Input, Output } from "valibot";

/**
 * Input schema type.
 */
export type InputSchema<
  TInput,
  TWrapped extends BaseSchema,
  TOutput = Output<TWrapped>,
> = BaseSchema<Input<TWrapped> | TInput, TOutput> & {
  /**
   * The schema type.
   */
  type: "input";
  /**
   * The wrapped schema.
   */
  wrapped: TWrapped;
};

/**
 * Creates a curried type-variant input schema function.
 *
 * @template TInput The type to be appended to the input type of the wrapped schema.
 *
 * @returns A curried function that creates a custom type-variant input schema.
 */
export const createInput = <TInput>() => {
  return <TWrapped extends BaseSchema>(
    wrapped: TWrapped,
  ): InputSchema<TInput, TWrapped> => {
    return {
      type: "input",
      expects: wrapped.expects,
      async: false,
      wrapped,
      _parse(input: unknown, config) {
        return this.wrapped._parse(input, config);
      },
    };
  };
};

This can be applied either like this...

const nullableInput = createInput<null>();

const RepeatSchema = nullableInput(v.picklist(['daily', 'weekly', 'monthly']));

type RepeatInput = v.Input<typeof RepeatSchema>; // "daily" | "weekly" | "monthly" | null
type RepeatOutput = v.Output<typeof RepeatSchema>; // "daily" | "weekly" | "monthly"

Or like this...

const RepeatSchema = createInput<null>()(v.picklist(['daily', 'weekly', 'monthly']));

type RepeatInput = v.Input<typeof RepeatSchema>; // "daily" | "weekly" | "monthly" | null
type RepeatOutput = v.Output<typeof RepeatSchema>; // "daily" | "weekly" | "monthly"

Certainly sub-optimal.

I'll completely understand if this isn't something you want to add to Valibot, but I'm very glad to have a solution!

@fabian-hiller
Copy link
Owner

Yes, that is correct. Thanks for the research! For now, I prefer to wait for feedback from other developers on this issue.

@fabian-hiller
Copy link
Owner

I think that this is now possible using our new pipe function:

import * as v from 'valibot';

const Schema1 = v.picklist(['foo', 'bar']);
const Schema2 = v.pipe(v.nullable(Schema1), Schema1);

type Input = v.InferInput<typeof Schema2>; // 'foo' | 'bar' | null
type Output = v.InferOutput<typeof Schema2>; // 'foo' | 'bar'

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

No branches or pull requests

2 participants