All Articles

TypeScript and nested form validation using Formik

When you’re dealing with forms, sooner or later you will be asked to introduce validation to it. This is so common that many libraries already simplify working with them. I use React so my weapon of choice was Formik.

I’m also a big fan of TypeScript, because once set up properly you can avoid a lot of stress related to incorrect types usage. However using TypeScript sometimes can be tricky.

Recently I’ve been working on a form which had one field that determined whether other fields should be visible or not. For example, let’s say we have a form in which we want define a bundle of files we want to deliver to someone. We can choose different ways of delivering the bundle. E.g. FTP or S3. Depending on our choice we want to display different fields. Two form mockups showing Bundle name field, and how to deliver the bundle dropdown. The has S3 selected and is showing two additional fields: Bucket and region. The second has FTP selected and is showing Host and Username fields.

Somewhere in the code we will need to define our configuration.

interface FormProps {
  bundleName: string;
  deliveryMethod: 's3' | 'ftp';
  configuration: DeliveryConfiguration;
}

type DeliveryConfiguration = S3Configuration | FTPConfiguration;

interface S3Configuration {
  bucket: string;
  region: string;
}

interface FTPConfiguration {
  host: string;
  username: string;
}

The way Formik validation works is that you define a function that accepts all form values, processees them and returns an error object. For any invalid object we should return an error message with a corresponding field. For example if the bundleName field is empty and also assuming we selected S3 and the bucket field is empty the error object is going to look like

{
  bundleName: 'Bundle name is required',
  configuration: {
    bucket: 'Bucket name is required'
  }
}

To fully laverage the power of TypeScript we can enforce the error object to only accept the keys that belong to our form. How to do this without rewriting the field names again?

We can use mapped types.

type errorsType = { 
  [key in keyof FormProps]?: string
}

This will create an object type whose keys can only be the ones that belong to the FormProps. And as they are optional (in case the field is valid) we added the ?. However this approach has one problem. configuration property is nested. And the current setup wants you to provide a string under configuration key.

To solve this problem we can use a special utility construct that TypeScript provides: Omit

type errorsType = { 
  [key in keyof Omit<FormProps, 'configuration'>]?: string
}

Now the type will accept all the fields from FormProps except configuration. There is just one thing left. We want the configuration property to be an object that accepts keys of either S3 or FTP configuration.

To do this we can use an intersection type

type errorsType = { 
  [key in keyof Omit<FormProps, 'configuration'>]?: string
} & {
  configuration: {
    [key in keyof (S3Configuration & FTPConfiguration)]?: string
  }
}

Two important things here. Notice that S3Configuration & FTPConfiguration are in parenthesis. And also notice that there is an intersection operator between them, not the union as we did with DeliveryConfiguration when defining FormProps. This is because the error object can contain all of them whereas the form itself has either S3 or FTP configuration.

I hope this article helped you understand how you can use the power of TypeScript to make your applications more bullet proof and how to avoid unnecessary work.

I encourage you to read TypeScript handbook to deepen your knowledge on this topic as it is a tool definitiely worth using.