valibot
valibot copied to clipboard
TS error when using custom()
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;
}),
],
);
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/
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: '',
});
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.
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 would return
false
instead of throwing and error and add the error message as the second parameter tocustom
. 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
andevery
validation action specifically for arrays. You could useevery
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?
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.
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.
With my current code pipeline actions accept a
dataset
as the first argument and return adataset
as a result. This will allow you to easily write your own actions and add as manyissues
as you want. I plan to publish a first draft next week.
Those are great news, I'm looking forward to testing it.
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.
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
}),
],
);
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?
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')
);
Thanks. I'll try that, do you plan on implementing it in valibot?
Yes, I will consider it. Do you have some good alternatives names for the action?
The only one that I could think of is iterate
.
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.
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.
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.
@fabian-hiller I was playing around and found that the forward
action is missing.
You are right. I will probably add it today or tomorrow. I will ping you then.
I am working on it and will probably add it later today.
Done
Thanks, I'll play with it again.
Is this issue resolved? I will close it for now, but will reopen it if not.
Feel free to create a new issue (or PR) to add an items
action in the next weeks.
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.
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 inotherEmails
theitems
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
})
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.
I agree, let's do the following, I'll close this issue and open a new one with a recap of this one.