valibot icon indicating copy to clipboard operation
valibot copied to clipboard

TS error when using custom()

Open ruiaraujo012 opened this issue 11 months ago • 21 comments

The property schema: 'object' gives me a TS error and I can't put it to work.

const FormSchema = v.object(
  {
    dataFimValidadeFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataFimValidadeInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataInicioValidadeFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataInicioValidadeInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataPedidoFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataPedidoInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    idEstado: v.optional(v.coerce(v.string(), String)),
    idInterno: v.optional(v.coerce(v.string(), String)),
    identificador: v.optional(v.string()),
    nome: v.optional(v.string()),
  },
  [
    v.custom((inputs) => {
      if (inputs.dataInicioValidadeInicio && inputs.dataInicioValidadeFim) {
        if (dateRangeValidator(inputs.dataInicioValidadeInicio, inputs.dataInicioValidadeFim)) {
          throw new ValiError([
            {
              input: inputs.dataInicioValidadeInicio,
              message: 'validation.minDateExceeded',
              origin: 'value',
              reason: 'date',
              validation: 'custom',
              path: [
                {
                  input: inputs,
                  key: 'dataInicioValidadeInicio',
                  schema: 'object', <--- Object literal may only specify known properties, and 'schema' does not exist in type 'PathItem'.
                  value: inputs.dataInicioValidadeInicio,
                },
              ],
            },
          ]);
        }
      }

      return true;
    }),
  ],
);

ruiaraujo012 avatar Mar 18 '24 12:03 ruiaraujo012

Don't throw inside of custom. Instead always return a boolean: https://valibot.dev/api/custom/

You can forward issues with this method: https://valibot.dev/api/forward/

fabian-hiller avatar Mar 19 '24 15:03 fabian-hiller

Hum, I saw some issues where people were throwing inside custom.

Imagine this scenario, how can I achieve the same result without throwing in custom?

codOutrasNacionalidades: v.array(v.string([v.minLength(1, 'required.otherNationalities')]), [
    v.minLength(1, 'required.otherNationalities'),
    v.custom((input) => {
      const duplicates = input.filter((item, index) => input.indexOf(item) !== index);

      const issues: v.SchemaIssues = [] as unknown as v.SchemaIssues;

      if (duplicates.length > 0) {
        input.forEach((val, index) => {
          if (duplicates.includes(val)) {
            issues.push(
              createValibotIssue({
                fieldPath: `pessoa.codOutrasNacionalidades.${index}`,
                input: { 'pessoa.codOutrasNacionalidades': input },
                message: 'validation.uniqueNationalities',
                reason: 'array',
              }),
            );
          }
        });
      }

      if (issues.length > 0) {
        throw new v.ValiError(issues);
      }

      return true;
    }),
  ]),
export const createValibotIssue = ({
  message,
  reason,
  input,
  fieldPath,
}: Pick<SchemaIssue, 'message' | 'reason'> & { input: Record<string, unknown>; fieldPath: string }): SchemaIssue => ({
  context: '',
  expected: null,
  input: input[fieldPath],
  message,
  path: [
    {
      input,
      key: fieldPath,
      origin: 'value',
      type: 'object',
      value: input[fieldPath],
    },
  ],
  reason,
  received: '',
});

ruiaraujo012 avatar Mar 19 '24 17:03 ruiaraujo012

I would return false instead of throwing and error and add the error message as the second parameter to custom. But currently there is no way to return multiple issues, but I am in the process of rewriting Valibot and it will be easier then.

fabian-hiller avatar Mar 20 '24 16:03 fabian-hiller

Also we have a some and every validation action specifically for arrays. You could use every to check for duplicate values.

  • https://valibot.dev/api/some/
  • https://valibot.dev/api/every/

fabian-hiller avatar Mar 20 '24 16:03 fabian-hiller

I would return false instead of throwing and error and add the error message as the second parameter to custom. But currently there is no way to return multiple issues, but I am in the process of rewriting Valibot and it will be easier then.

I've seen the discussion, I'm waiting for that :).

Also we have a some and every validation action specifically for arrays. You could use every to check for duplicate values.

  • https://valibot.dev/api/some/
  • https://valibot.dev/api/every/

I didn't try it yet, but do every put the error at the index of the element?

ruiaraujo012 avatar Mar 20 '24 16:03 ruiaraujo012

I didn't try it yet, but do every put the error at the index of the element?

No, but that's an interesting idea!

I've seen the discussion, I'm waiting for that

With my current code pipeline actions accept a dataset as the first argument and return a dataset as a result. This will allow you to easily write your own actions and add as many issues as you want. I plan to publish a first draft next week.

fabian-hiller avatar Mar 20 '24 16:03 fabian-hiller

No, but that's an interesting idea!

I think so, in my current implementation, this is important to select the duplicated option, and with the code that I've shared here, it works. image

With my current code pipeline actions accept a dataset as the first argument and return a dataset as a result. This will allow you to easily write your own actions and add as many issues as you want. I plan to publish a first draft next week.

Those are great news, I'm looking forward to testing it.

ruiaraujo012 avatar Mar 20 '24 17:03 ruiaraujo012

I plan to publish a blog post in two or three weeks about the results and investigations of the new pipe function implementation as well as the rewrite of the library. For now, I expect the bundle size to be smaller, the performance to be faster and the types to be more accurate.

fabian-hiller avatar Mar 21 '24 15:03 fabian-hiller

Although it's not recommended, I'll let v.custom with throw as it is working as I need, I'll come here again when the rewrite is done to update it. And perhaps close the issue then with a solution.

In summary, I would like to have:

  • An option to validate every element of the array and add the issue to the index of the occurrence (in this example, otherNationalities.2.
  • An option to add issues globally to the schema based on field relations:
const Schema = v.object(
  {
     mainNationality: v.string([v.minLength(1, 'required.nationality')]),
     otherNationalities: v.array(v.string([v.minLength(1, 'required.otherNationalities')]), [
        v.minLength(1, 'required.otherNationalities'),
        (custom valiration to check duplicated)
     ])
    (...other fields)
  },
  [
    v.<someMethod>((inputs) => {
      // If some element in otherNationalities is equal to the mainNationality, 
      // add an issue to the otherNationalities item index
    }),
  ],
);

ruiaraujo012 avatar Mar 21 '24 16:03 ruiaraujo012

Hi @fabian-hiller I saw your comment on the rewrite PR, I was thinking about testing the new implementation of pipe in this context, do you have any suggestion on where I can start? Or there are no way to do this yet?

ruiaraujo012 avatar May 10 '24 08:05 ruiaraujo012

It is possible, but you have to write your own action. The concept is very simple. Here is an example where I have called the action "items":

import {
  _addIssue,
  type BaseIssue,
  type BaseValidation,
  type ErrorMessage,
} from 'valibot';

/**
 * Items issue type.
 */
export interface ItemsIssue<TInput extends unknown[]>
  extends BaseIssue<TInput> {
  /**
   * The issue kind.
   */
  readonly kind: 'validation';
  /**
   * The issue type.
   */
  readonly type: 'items';
  /**
   * The expected input.
   */
  readonly expected: null;
  /**
   * The validation function.
   */
  readonly requirement: (input: TInput[number]) => boolean;
}

/**
 * Items action type.
 */
export interface ItemsAction<
  TInput extends unknown[],
  TMessage extends ErrorMessage<ItemsIssue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, ItemsIssue<TInput>> {
  /**
   * The action type.
   */
  readonly type: 'items';
  /**
   * The action reference.
   */
  readonly reference: typeof items;
  /**
   * The expected property.
   */
  readonly expects: null;
  /**
   * The validation function.
   */
  readonly requirement: (input: TInput[number]) => boolean;
  /**
   * The error message.
   */
  readonly message: TMessage;
}

/**
 * Creates an items validation action.
 *
 * @param requirement The validation function.
 *
 * @returns An items action.
 */
export function items<TInput extends unknown[]>(
  requirement: (input: TInput[number]) => boolean
): ItemsAction<TInput, undefined>;

/**
 * Creates an items validation action.
 *
 * @param requirement The validation function.
 * @param message The error message.
 *
 * @returns An items action.
 */
export function items<
  TInput extends unknown[],
  const TMessage extends ErrorMessage<ItemsIssue<TInput>> | undefined,
>(
  requirement: (input: TInput[number]) => boolean,
  message: TMessage
): ItemsAction<TInput, TMessage>;

export function items(
  requirement: (input: unknown) => boolean,
  message?: ErrorMessage<ItemsIssue<unknown[]>>
): ItemsAction<unknown[], ErrorMessage<ItemsIssue<unknown[]>> | undefined> {
  return {
    kind: 'validation',
    type: 'items',
    reference: items,
    async: false,
    expects: null,
    requirement,
    message,
    _run(dataset, config) {
      if (dataset.typed) {
        for (let index = 0; index < dataset.value.length; index++) {
          const item = dataset.value[index];
          if (!this.requirement(item)) {
            _addIssue(this, 'item', dataset, config, {
              input: item,
              path: [
                {
                  type: 'array',
                  origin: 'value',
                  input: dataset.value,
                  key: index,
                  value: item,
                },
              ],
            });
          }
        }
      }
      return dataset;
    },
  };
}

Now you can write the following code:

import * as v from 'valibot';
import { items } from './your-path';

const Schema = v.pipe(
  v.array(v.string()),
  items((item) => item.startsWith('foo'), 'Your message')
);

fabian-hiller avatar May 10 '24 17:05 fabian-hiller

Thanks. I'll try that, do you plan on implementing it in valibot?

ruiaraujo012 avatar May 10 '24 18:05 ruiaraujo012

Yes, I will consider it. Do you have some good alternatives names for the action?

fabian-hiller avatar May 10 '24 20:05 fabian-hiller

The only one that I could think of is iterate.

ruiaraujo012 avatar May 10 '24 22:05 ruiaraujo012

I was thinking, it would be nice if the callback had 2 or 3 arguments (item, index, items) => boolean.

With that we could create validations based on other items, for example, identify duplicated values.

items((item, index, items) => items.filter((v)=> v===item).length > 1, 'Duplicated')

Let me know what you think.

Another requirement I can think that would be great is an action to validate properties based on other properties. Let me give you an example:

{
    "primaryEmail": "[email protected]",
    "otherEmails": ["[email protected]", "[email protected]"]
}

To validate that the primaryEmail is not present in otherEmails the items action doesn't work, I need some actions that allow me to add issues globally.

I hope I didn't confuse you.

ruiaraujo012 avatar May 10 '24 22:05 ruiaraujo012

The only one that I could think of is iterate.

Thanks! Will think about it! Both names could work.

I was thinking, it would be nice if the callback had 2 or 3 arguments (item, index, items) => boolean.

I agree!

To validate that the primaryEmail is not present in...

This requires a custom validation in the pipeline of the object definition with a forward to the appropriate field. Note: We renamed custom to check with the rewrite.

fabian-hiller avatar May 11 '24 03:05 fabian-hiller

Oh, that's what I need, I thought that the custom was deprecated. Thanks. I'll play a bit with the current state of the lib.

Please update this issue when this action gets done.

ruiaraujo012 avatar May 11 '24 07:05 ruiaraujo012

@fabian-hiller I was playing around and found that the forward action is missing.

ruiaraujo012 avatar May 11 '24 17:05 ruiaraujo012

You are right. I will probably add it today or tomorrow. I will ping you then.

fabian-hiller avatar May 11 '24 18:05 fabian-hiller

I am working on it and will probably add it later today.

fabian-hiller avatar May 15 '24 18:05 fabian-hiller

Done

fabian-hiller avatar May 15 '24 22:05 fabian-hiller

Thanks, I'll play with it again.

ruiaraujo012 avatar May 16 '24 06:05 ruiaraujo012

Is this issue resolved? I will close it for now, but will reopen it if not.

fabian-hiller avatar May 23 '24 16:05 fabian-hiller

Feel free to create a new issue (or PR) to add an items action in the next weeks.

fabian-hiller avatar May 23 '24 16:05 fabian-hiller

Is this issue resolved? I will close it for now, but will reopen it if not.

I'll check it ASAP and let you know.

Feel free to create a new issue (or PR) to add an items action in the next weeks.

I'll open one issue latter today.

ruiaraujo012 avatar May 23 '24 16:05 ruiaraujo012

I'm not able to put a schema to work with this requirement, can you help here?

{
    "primaryEmail": "[email protected]",
    "otherEmails": ["[email protected]", "[email protected]"]
}

To validate that the primaryEmail is not present in otherEmails the items action doesn't work, I need some actions that allow me to add issues globally.

The forward action doesn't work because I cannot put the key and index of the duplicated email, in this case otherEmails.1.

One possible solution that came to my mind is a method that allow us to add issues to the list of issues already created by valibot validations. Something like:

v.someMethod((inputs, ctx) => {
// ...(my custom validation with ctx.addIssue())...
return ctx.issues // Or other type of return
}) 

ruiaraujo012 avatar May 23 '24 18:05 ruiaraujo012

Maybe we should add an advanced refine method (similar to Zod's superRefine) that allows you to directly interact with the input and output dataset of the method and not only with the raw value.

fabian-hiller avatar May 23 '24 19:05 fabian-hiller

I agree, let's do the following, I'll close this issue and open a new one with a recap of this one.

ruiaraujo012 avatar May 23 '24 19:05 ruiaraujo012