[SURVEY RESULTS] The 2024 edition of State of Software Modernization market report is published!
GET IT here

Enhanced Type Safety with Zod - Tame Badly Typed External Libraries

readtime
Last updated on
November 2, 2023

A QUICK SUMMARY – FOR THE BUSY ONES

How Zod boosted type safety

In our projects, we use AdminJS, an external library that provides a GUI for managing database records. It's a great tool for rapidly creating CRUD interfaces for our clients.

However, we ran into some issues with poorly typed code inside the library that made it difficult to integrate with our own custom logic. This is where Zod, a validation library, came to the rescue.

Zod enabled us to create robust type definitions and validate data against those definitions. This helped us to avoid runtime errors and catch issues early in development.

In this article, we'll take a look at how Zod helped us to deal with the poorly typed code in AdminJS and how it improved the overall quality of our code.

TABLE OF CONTENTS

Enhanced Type Safety with Zod - Tame Badly Typed External Libraries

Introduction

The article highlights how the utilization of Zod, a validation library, significantly improved type safety within a project. By addressing issues stemming from poorly typed code in AdminJS, Zod enabled the creation of robust type definitions and data validation processes. This proactive approach helped prevent runtime errors and enhanced the overall quality of the codebase.

Challenges with poorly typed external library

When using external libraries, it's not uncommon to run into issues with poorly typed code. This was the case with AdminJS, a library that we used in our projects to provide a GUI for managing database records. While AdminJS is a great tool for rapidly creating CRUD interfaces for our clients, it also presented some challenges.

One of these challenges was creating schemas for the objects that we received from AdminJS. The library provides a Record<string, any> type, which is not particularly useful when it comes to type safety. We needed to define more robust type definitions for our data to avoid runtime errors.

This is where Zod, a validation library, came into play. We used Zod to create schemas for the objects that we received from AdminJS, which enabled us to catch errors early in development and avoid issues in production.

The code snippet above demonstrates how we defined a schema for a dashboard object that we received from AdminJS. We used Zod's object method to create an object schema with three properties: title, project, and type. We also used Zod's nativeEnum method to create an enumeration schema for the type property, which accepts two specific string values.

By creating these schemas with Zod, we were able to define the exact shape of the data we expected to receive, and catch any errors if the data did not match that shape. This helped us to ensure the quality of our code and avoid any issues caused by the poorly typed code in AdminJS.

Creating SubSchemas to Handle Specific Data

In the previous section, we defined a schema for the entire payload that we receive from AdminJS. However, in some cases, we might only be interested in a specific part of the payload, or we might not need all the fields from the payload. In such cases, it is useful to create sub-schemas, which define a subset of the original schema.

ZOD provides several methods to create sub-schemas, such as pick, omit, and partial.

In the code snippet provided, we are using these methods to create three different sub-schemas from the original DashboardPayloadSchema:

  • DashboardTypeSchema: This schema picks the 'type' field from the DashboardPayloadSchema and creates a new schema with only that field.
  • DashboardPartialSchema: This schema creates a partial schema from the DashboardPayloadSchema, which means that all the fields are optional.
  • DashboardWithoutTypeSchema: This schema omits the 'type' field from the DashboardPayloadSchema and creates a new schema with only the 'title' and 'project' fields.

Creating sub-schemas can be useful when we want to validate only a part of the object, or when we want to reuse some of the fields in a different schema. It can also help in simplifying the validation logic and making it more readable.

Refining Schemas with Custom Validation

One of the key features of Zod is the ability to refine schemas with custom validation logic. The refine method can be used to add validation rules to a schema, allowing you to ensure that data meets specific requirements before it is processed.

In the following code snippet, we use refine to ensure that the title field in our DashboardPayloadSchema is no longer than 255 characters. If the title is too long, Zod will throw an error with a custom message.

The refine method can also accept asynchronous functions, as shown in the following example. Here, we use refine to ensure that the project field in our schema exists in a database. If the project does not exist, Zod will throw an error with a custom message.

Modifying Validated Values with transform()

While validation ensures that the received data conforms to the schema, sometimes you might need to transform the data to a different format or structure. For example, you might need to extract a certain substring from a string or make a database call to fetch additional data based on a received value.

Zod provides the transform() method that allows you to modify the validated values before returning them. The transform() method accepts a synchronous or asynchronous function that takes the validated value and returns the transformed value.

In the following code snippet, we define a DashboardProject schema that refines the received project string to ensure that it starts with the "PR_" prefix. Then, we use the transform() method to remove the prefix before returning the value.

You can also use an asynchronous function with transform() to perform more complex operations, such as making a database call:

In the above example, the getProjectObjectFromDB() function is an asynchronous function that fetches the project object from the database based on the received project string. The transform() method applies this function to the validated value and returns the result.

Inferring Types and Creating Type Guards

In the above code snippet, the infer method is used to automatically infer the type of the schema defined by DashboardPayloadSchema. The inferred type is then assigned to a type alias called Dashboard. This allows us to use the inferred type throughout our codebase without having to manually define it.

Next, the code exports a type guard function called isDashboard. A type guard is a function that checks if a value is of a certain type at runtime. In this case, the isDashboard function checks if the provided payload conforms to the Dashboard type, by attempting to parse the payload using the DashboardPayloadSchema. If the parsing succeeds, the function returns true, indicating that the payload is a valid Dashboard. If the parsing fails, the function returns false.

Using type guards like isDashboard can help catch type errors at runtime and make our code more robust, especially when working with data from external sources like APIs or databases where the shape of the data may not be known in advance.

Summary

In this article, we explored how Zod, a validation library, came to the rescue when integrating AdminJS, an external library with poorly typed code. AdminJS provides a Record<string, any> type, leading to type safety issues. Zod helped us create robust type definitions and validate data against those definitions, catching errors early in development.

We defined a schema for a dashboard object using Zod's object and nativeEnum methods, ensuring the expected data shape. We also created sub-schemas with pick, omit, and partial for specific parts of the payload. Custom validations were added with the refine method to enforce requirements like string length and database existence.

Zod's transform method allowed us to modify validated values, and we learned how to infer types using infer, creating type guards to catch type errors at runtime. Overall, Zod improved code quality and reduced runtime errors, making our integration with AdminJS more efficient and reliable.

Frequently Asked Questions

No items found.

Our promise

Every year, Brainhub helps 750,000+ founders, leaders and software engineers make smart tech decisions. We earn that trust by openly sharing our insights based on practical software engineering experience.

Authors

Marcin Żmudka
github
JavaScript Software Engineer

Experienced full-stack developer and team leader, enthusiast of innovations and user-centric solutions.

Read next

No items found...

Get smarter in engineering and leadership in less than 60 seconds.

Join 300+ founders and engineering leaders, and get a weekly newsletter that takes our CEO 5-6 hours to prepare.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.