Open agnar-nomad opened 1 month ago
Here is an example using parse
and another using safeParse
. I have removed the framework specific code.
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.pipe(v.string(), v.nonEmpty(), v.email()),
password: v.pipe(v.string(), v.nonEmpty(), v.minLength(6)),
});
type LoginSchema = typeof LoginSchema;
let formErrors: Partial<Record<v.IssueDotPath<LoginSchema>, string>> = {};
const handleLogin = async (data: unknown) => {
try {
formErrors = {};
const result = v.parse(LoginSchema, data);
// Handle login ...
} catch (error) {
if (v.isValiError<LoginSchema>(error)) {
const flatIssues = v.flatten<LoginSchema>(error.issues);
for (const key in flatIssues.nested) {
formErrors[key] = flatIssues.nested[key][0];
}
} else {
throw error;
}
}
};
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.pipe(v.string(), v.nonEmpty(), v.email()),
password: v.pipe(v.string(), v.nonEmpty(), v.minLength(6)),
});
type LoginSchema = typeof LoginSchema;
let formErrors: Partial<Record<v.IssueDotPath<LoginSchema>, string>> = {};
const handleLogin = async (data: unknown) => {
formErrors = {};
const result = v.safeParse(LoginSchema, data);
if (result.success) {
// Handle login ...
} else {
const flatIssues = v.flatten<LoginSchema>(result.issues);
for (const key in flatIssues.nested) {
formErrors[key] = flatIssues.nested[key][0];
}
}
};
With this setup I still get a few errors:
const [formErrors, setFormErrors] = useState<Partial<Record<v.IssueDotPath<LoginSchemaType*#1>, string>>>({});
const handleLogin = async () => {
try {
v.parse(LoginSchema, formData)
// api call
} catch (error) {
if (v.isValiError<LoginSchemaType*#1>(error)) {
const flatIssues = v.flatten<LoginSchemaType*#1>(error.issues);
for (const key in flatIssues.nested) {
formErrors[key]*#2 = flatIssues.nested[key][0]*#3;
}
} else {
throw error;
}
}
};
I tried to highlight the error locations inside the code with the symbols *#[issue_number]
The issues are:
issue # 1
Type '{ email: string; password: string; }' does not satisfy the constraint 'BaseSchema<unknown, unknown, BaseIssue<unknown>> | BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>'.ts(2344)
issue # 2
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Partial<Record<never, string>>'. No index signature with a parameter of type 'string' was found on type 'Partial<Record<never, string>>'.ts(7053)
issue # 3 Object is possibly 'undefined'
I would really appreciate some more help on this
What is your implementation of LoginSchemaType
? It should be typeof LoginSchema
and not v.InferInput<typeof LoginSchema>
.
You can try out my code in our playground: https://valibot.dev/playground/
Thanks for your answers.Yes I glanced over the type definition.... If I use typeof LoginSchema
some of the errors go away.
But not all. Especially issue # 2 from above still remains. And it manifests the same even if I paste your code in the playground.
Also could you explain what the difference between InferInput
and typeof Schema
is in this instance? WHy is it a rpoblem which one I use here? Is it only that the isValiError
method expects a different type input?
Use typeof Schema
to reference the type of a schema and InferInput<typeof Schema>
to reference its input type. Please share a playground link if you can't fix it. I am happy to help you.
What is the practical difference between type of schema and input type ?
What is the practical difference between type of schema and input type?
The type of a schema stores all the type information of a schema, including the input, output, and issue type. The input type represents only the expected input type of a schema.
I literally just copy + paste your code into the playground:
Sorry, it worked fine in my editor. You could use type casting since you know that key
is a matching dot path, or just ignore the TS error.
The type of a schema stores all the type information of a schema, including the input, output, and issue type. The input type represents only the expected input type of a schema.
Thank you
Sorry, it worked fine in my editor.
For completeness sake, I need to attach a screen of it I did want to avoid manual overrides, if possible
The problem is that for (const key ...)
changes v.IssueDotPath<LoginSchema>
to string
. I think there is nothing we can do about this because object keys are always converted to string
when using the default loop syntax.
Okay, I ended up doing something like this. And it works well enough.
const someSchema = v.object({
// ... definition
})
type MySchemaType = typeof someSchema
type FormErrorKey = v.IssueDotPath<MySchemaType>
const formErrors: Partial<Record<FormErrorKey, string>> = {};
const handleSubmit = async () => {
try {
// validate form data
v.parse(someSchema, { ...formData })
// send data to API
await callAsyncApi({ ...formData });
} catch (error) {
if (v.isValiError<MySchemaType>(error)) {
const flatIssues = v.flatten<MySchemaType>(error.issues)
for (const key in flatIssues.nested) {
formErrors[key as FormErrorKey] = flatIssues.nested[key as FormErrorKey]![0]; // I hope that this line could be improved in the future
}
if(Object.keys(formErrors).length) {
renderErrorMessages(formErrors)
}
} else {
console.error("Edit Link Error", error)
throw error
}
}
};
I hope this could be useful to someone
@agnar-nomad - It looks from your last message like validation errors are part of your flow (given the renderErrorMessages function) and not an exception. If that is the case, have you considered using safeParse instead?
Hello @fabian-hiller -
If I am parsing multiple schemas in a function, is there a way to catch ValiError for all possible parsing error?
I am currently adding multiple checks:
try {
const address = parse(Address, policyOrderDetails);
const vehicle = parse(Vehicle, policyOrderDetails);
} catch (e) {
if (isValiError<typeof Vehicle>(e)) {
console.error(
'Error while parsing vehicle data: ',
flatten<typeof Vehicle>(e.issues)
);
}
if (isValiError<typeof Address>(e)) {
console.error(
'Error while parsing address data: ',
flatten<typeof Address>(e.issues)
);
}
throw e;
}
The TypeScript generic is just type information. At runtime, your second if statement will never be triggered. You should rewrite it:
If you don't use/care about the exact type information, you can remove the TypeScript generics.
type Vehicle = typeof Vehicle;
type Address = typeof Address;
try {
const address = parse(Address, policyOrderDetails);
const vehicle = parse(Vehicle, policyOrderDetails);
} catch (error) {
if (isValiError<Vehicle | Address>(error)) {
console.error(
'Error while parsing vehicle or address data: ',
flatten<Vehicle | Address>(e.issues)
);
}
throw error;
}
agnar-nomad - It looks from your last message like validation errors are part of your flow (given the renderErrorMessages function) and not an exception. If that is the case, have you considered using safeParse instead?
I have considered it, but I do not use it now. There is no particular reason why, just personal preference I guess.
If I am parsing multiple schemas in a function, is there a way to catch ValiError for all possible parsing error?
Personally this is quite the situation where I would use safeParse
so that I can easily identify and process the exact error.
Similar to how Fabian said earlier, if you have an error in your first schema validation (inside the try
clause), you won’t even get to processing the second one, so you can’t get all the errors in your setup that you show here.
I would like to see how to correctly handle the ValiError when parsing a schema. In a type-safe manner. For example in this login form handling demo:
I am getting a crazy type issue on
Object.entries(flatIssues.nested)
at this point. More importantly, how to type theformErrors
variables? I would love to see the team's approach to this.