Using yup and typescript for typesafe select validation

Daniel Voigt
3 min readJun 5, 2022

Validating user input can be quite cumbersome from time to time, especially with predefined select values. Your back-end is expecting a predefined set of values and you should validate them both in your front-end as well as your back-end.

Multiple libraries come to mind. I chose yup for no particular reason, but the use of conditional validation and the mixed type seems quite reasonable to use. The tricky part is to define the selection values one time as a single source of truth and use them in all your subsequent components.

The most straightforward thing which comes to mind is just to define a string literal union and be done with it

This works, but screams DRY. So let’s pull out the array

A lot better, but there is still one thing which bothers me a lot. If we define the select values only once why do we need to explicitly define a type and don’t let typescript infer the type of our array? Changing first the type and then the array might seem trivial, but can become quite ugly.

We’ll start by infering the type of our array

The type of SelectType becomes string and not the string literal union we wanted. This is because of type widening. Typescript automatically infers the string type for our array, which is actually correct because our variable might be const but adding new values to our array is still valid since adding values to our array does not change the variable itself.

This can be prevented with const assertions.

We no longer are able to either change the variable nor change the array itself, which gives us the correct type of our initial string literal union.

Looks like the solution… Until you use the array with yup:

The type 'readonly ["glas", "stone", "plastic"]' is 'readonly' and cannot be assigned to the mutable type any[]

This kinda sucks and there is an open issue, but until then we need to find another solution. The lazy way would be to just use the spread operator to create a mutable copy

which is kinda ugly.

Before actually solving this issue, let’s incorporate one more thing. Often times (especially with yup validations) you want to validate some kind of form. For the form itself, we don’t only have the values, but also the label names. I found it very useful to just create a dictionary for those label value pairs to have one single source of truth for all my selection options.

Infering our type from the array itself

and our type is widened again, so we should use our const assertion trick

This still doesn’t work. In TypeScript there are two ways to get typings. Either we explicitly define what type our variable has to be, or we let the compiler infer the type. Since we explicitly typed our variable TypeScript will only check if the value we assign to our variable conforms to this type, but will lose any additional information. A good start is to switch to type inference

Which works, but leaves us with 2 gotchas: yup will complain about the read-only array and errors in our array itself are most likely not caught. Who hasn’t typed ‘lable’ instead of ‘label’? So we know we have to let the compiler infer the type, but we still want to restrict what we infer. And there is a solution. Using a generic function which infers our type but at the same time restricts it.

Let’s break this function down:

T extends Array<LabelValuePair> restricts the generic type T to our LabelValuePair type

Array<{value: V}> extracts another type V from the LabelValuePair inside the array

V extends string actually prevents the compiler from widening our string literals to the string type.

This conveniently also fixes our read-only issue with yup. At least somewhat. We need to iterate over our label value pair and extract only the values, but this seems fair to me. Our final code looks like this

And finally we can make use of yups utility type InferType, to get strong typings for our form library of choice.

For my use case this is the ideal solution. We have strong typings for our LabelValuePair , we only need one single variable to hold all the values and the type itself is inferred. A good opportunity to learn about type widening, inference and generics!

--

--

Daniel Voigt

Software developer, language enthusiast and Jazz musician.