Declare Precise Array Types within Zod Schemas

This lesson demonstrates how to provide runtime type-safety for arrays in TypeScript applications.

First we need to understand the challenges behind ensuring array type-safety in TypeScript. We explain how TypeScript infers arrays with an undefined length, making it difficult to guarantee that array accesses are safe.

Zod schemas provide the z.array to define arrays of various element types.

We compare how utilities such as .min, .max and .nonempty get reflected in inferred TypeScript types, and how zod processes the validation.

Share with a coworker

Transcript

[00:00] We need to know that guaranteeing array type safety is a tricky thing to do in TypeScript. Let's take a look at this array over here. We can see that even though we pass exactly three elements to this literal, then TypeScript is going to infer it as an array of numbers, an array of undefined length, right? So TypeScript cannot figure out what is the exact length in compile time. Now if we try to access a certain item over here then TypeScript would say that hey this is a number and it doesn't matter that this is way out of range of this array.

[00:35] Now why does it happen? If we take a look at the internal TypeScript's array interface we will see that the length is just a number. So it's not a type parameter somewhere here that would get evaluated somehow, basing on what is the literal itself, it's just a plain number. So we'll also scroll down to see that this index signature is going to return the T, so whatever is the element of the array, for any number that we pass over here. So, we could also get some really stupid results, such as passing the math.py is going to return a number, or even if we pass infinity, which also doesn't make sense, this is still going to be a number.

[01:20] So having said that let's use array-based Zodd schemas. Here we've got a guest detail schema with properties such as name, age, email, whatever the guests are and we're going to use that schema inside a nested property inside our nested room booking schema that we've used before so we're going to create a new property called guest details and in order to define arrays in Zod we're just creating Z dot array and we pass an existing schema, whether it's object-based, primitive-based, whatever that is, it's just going to be wrapped in an array. So what we have over here is we can use a little bit of useful utilities. And in this case, we can use that, let's say, min one, since let's say that there might be at least one guest for a certain booking. And let's say that it's not possible to provide more than four guests the same way as we have over here with the number of guests.

[02:21] So let's take a look at what we know about the type. So type T equals NZ dot infer of the type of of this schema. And what we can see is that the guest details is just simply an array. So due to the fact that TypeScript doesn't track the information about the length of the array, not a tuple, but an array, the information about the min and max is gone. So let's take a look at another utility.

[02:51] So let's pass guest details, which is non-empty. So this is going to be another utility. So z.array of guest details schema, and here we're going to put the non-empty. Now non-empty is going to be an array which needs to have at least a single element. So if we take a look at here, here we can see that this is a variadic tuple which is an array itself and it needs to have at least one element and dynamic number of elements over here.

[03:30] So it could be one in total, providing that this is essentially empty, but it could be one and whatever number of elements we need over here. So this one is going to be type safe only in runtime and this one is going to be type safe in both compile time and run time, we just need to remember that non-empty checks only for existing of at least one element so it's not more specific than that. So let's just rename T to make it more explicit that this is a booking item. And let's prove that guestDetailsNotEmpty is going to be type safe in both runtime and compile time. So let's uncomment that.

[04:13] Obviously, now everything works since we have no annotation. And when we annotate it into booking, then guessDetails is okay since this information is lost on TypeScript level. However, non-empty is going to provide type safety in compile time as well. And as we can see, source has no elements, but target requires one, at least one. So let's also run this file against ts-node and what we can see is that the file doesn't even compile and it's ts-node specific that since the file doesn't compile it's not even going to be executed.