fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
5.91k stars 181 forks source link

Add an example for handling ValiError errors #779

Open agnar-nomad opened 1 month ago

agnar-nomad commented 1 month ago

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:


const LoginSchema = v.object({
  email: v.pipe(v.string(), v.nonEmpty(), v.email() ),
  password: v.pipe(v.string(), v.nonEmpty(), v.minLength(6) ),
});

const [formData, setFormData] = useState<LoginSchemaType>({
    email: '',
    password: '',
  });
const [formErrors, setFormErrors] = useState<`What HERE?`>();

const handleLogin = async () => {
    setFormErrors({});

    try {
      v.parse(LoginSchema, formData)
      await loginMutation(formData)
    } catch (error) {
      if (error instanceof v.ValiError && error.issues) {
        const flatIssues = v.flatten<typeof LoginSchema>(error?.issues)
        const newErrors = {};

        Object.entries(flatIssues.nested).forEach(([key, value]) => {
          newErrors[key] = value[0]
        })

        setFormErrors(newErrors);
      } else {
          console.error(error)
      }
    }
  };

I am getting a crazy type issue on Object.entries(flatIssues.nested) at this point. More importantly, how to type the formErrors variables? I would love to see the team's approach to this.

fabian-hiller commented 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];
    }
  }
};
agnar-nomad commented 3 weeks ago

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

fabian-hiller commented 3 weeks ago

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/

agnar-nomad commented 3 weeks ago

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?

fabian-hiller commented 3 weeks ago

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.

agnar-nomad commented 3 weeks ago

What is the practical difference between type of schema and input type ?

I literally just copy + paste your code into the playground: https://valibot.dev/playground/?code=JYWwDg9gTgLgBAKjgQwM5wG5wGZQiOAcg2QBtgAjCGQgbgCh6BjCAO1XgBkIBzYVgMpMAFgFMQyOAF5MAOggUAVqKYwAFAG96cOOOTBSALjlhgYUWoyyOUfjzUBKADRzWbAKLgYAT0curegaOztpwYGioAO7QACbGVqbmltYwtqz2zq4eXr6ZViD8nKLpMMJqAGwOIQC+Dgz0PuZw3HyCInrScI2iENjNvPxCYhL1pKLw2NAg7lB4UKjGAArIsMBkADwASiqx61YAkqioAK6iACLUy6XrLYPtEgB8LjZ2Dw+dGtX1LOzwwsisGJjW6sTpobysJhwNQxZAwZDGY6sADWbkirAc0neWh0kyg01m0HQMk+DB0Pw4cCgohOpHgMisqGQ2FEy3mFhBQz0Llh8LqoWAfTU1NpMGsxyYTBpqExOJ0cAA9Aq4AAJAFA0RwUgDUGyPWhaq6UioTVy8lsSnYUhww4nGmdKxWuEwYo3HVcx7CmnHOmyYBHU4ysnyvHQinwZGibxwfg4a0wW2B2SsGkumKy0LynBTGZzVAAbUj3gAup0nQmAzTk6nRDFC1Hi-mAAzF4M6aoG+hfIA

fabian-hiller commented 3 weeks ago

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.

agnar-nomad commented 3 weeks ago

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 image

fabian-hiller commented 3 weeks ago

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.

agnar-nomad commented 3 weeks ago

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

mxa0079 commented 1 week ago

@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?

mxa0079 commented 1 week ago

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;
  }
fabian-hiller commented 1 week ago

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 commented 1 week ago

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.