Dynamic Type Validation in TypeScript

Dynamic Type Validation in TypeScript

TypeScript performs static type validation. Developers should take advantage of dynamic validations. Dynamic type validator in TypeScript. Dynamic type validation allows a type to generate a validator from its definition. If you are working in a TypeScript codebase, I recommend you give the dynamic validations method of validating your objects a try

There is no doubt that TypeScript has enjoyed a huge adoption in the JavaScript community, and one of the great benefits it provides is the type checking of all the variables inside our code. It will check if performing any operation on a variable is possible given its type.

Most people think that by using TypeScript as their application language, they are “covered” from any emptiness error, like the classic “undefined is not a function” or, my favorite, “can’t read property X of undefined.” This assumption is wrong, and the best way to demonstrate it is with code!

I gave a talk on this topic at the TypeScript Berlin Meetup. This article and the talk cover the same content, so you can use either to learn about this topic!

Why TypeScript won’t always cover you 🕵

The following example does not present any TypeScript error.

// Typescript definition
type ExampleType = {
  name: string,
  age?: number,
  pets: {
    name: string,
    legs: number,
  }[],
};

// communicates with external API
const fetchData = (): Promise<ExampleType> => {};

const getBiped = async () => {
  const data = await fetchData();
  console.log(data);
  // { name: 'John' }
  return data.pets.find(pet => pet.legs === 2); // Boom!
};

The snippet contains:

  • ExampleType – a type definition with two properties required, name and pets, and one optional property, age. The property pets is an array of objects with name and legs, both required
  • fetchData – a function to retrieve data from an external endpoint
  • getBiped – another function that will call fetchData, iterate over the pets, and return only the pets with two legs

So, why will my script fail when I execute it? The reason is because the external API is returning an object that doesn’t contain pets inside, and then when you try to execute data.pets.find(), you will receive the error Uncaught ReferenceError: Cannot read property 'find' of undefined.

In the official React documentation, you can find a very nice definition of what TypeScript is:

TypeScript is a programming language developed by Microsoft. It is a typed superset of JavaScript and includes its compiler. Being a typed language, TypeScript can catch errors and bugs at build time, long before your app goes live.

Given that definition, it’s possible to formulate a new assumption:

TypeScript performs static type validation. Developers should take advantage of dynamic validations.

Do you need to validate everything? 🤔

Simply put, no! 🎉

Checking all the variables in our application is time-consuming from both a development and performance perspective. A nice rule of thumb to follow is:

Validate all the external sources of your application.

External sources are everything that is external or doesn’t have access to your application. Some examples:

  • API responses
  • Content inside files
  • Input from the user
  • Untyped libraries

An application will always present at least one external source, otherwise, it would very likely be useless. Therefore, let’s take a look at how you can write validations for your objects in TypeScript.

To keep things simple, the original snippet will be considered the base, and on top, I will show how to implement each of the validation methods.

Manual validation

The most basic validation, it’s a set of conditions that check whether the structure is the expected one.

const validate = (data: ExampleType) => {
  if (!data.pets) return false;
  // perform more checks

  return true;
};

const getBiped = async () => {
  const data = await fetchData();
  console.log(data);
  // { name: 'John' }

  if (!validate(data))
    throw Error('Validation error: data is not complete ...');

  return data.pets.find(pet => pet.legs === 2);
};

As you can see, a new function has been defined, called validate, which receives as a parameter an ExampleType object, with which it checks whether the property pets is defined. If it is not, it will return false, which will end up throwing an error with a description message. Otherwise, it will continue the execution, and now, when evaluating data.pets.find, it won’t throw an error.

Be aware that the implementation of the validate function is quite simple, and there is room for many more checks, such as:

  • name should exist
  • name should be a string
  • If age exists, it should be a number
  • pets should be an array of objects
  • Each pet object should have props name and legs

The more checks you add, the more robust your application will be — but the more time you need to invest, too.

The advantages of this method are:

  • No external libraries required: Only pure TypeScript
  • Business-centric: You can add any business logic inside these validators. For example, you can check that propertyA shouldn’t exist if propertyB is present

It also presents some disadvantages:

  • Manual work: Every validation has to be manually coded, and this can be quite time-consuming
  • Duplication of code: In the example, ExampleType already defines that there is a pets property, and that it is required. But again, inside the validation code, you should still check that it’s true
  • Room for bugs: In the previous code, there were many “bugs” or places for improvement

Using a validation library

Why reinvent the wheel, right? This method consists of using any validation library to assert the structure of the objects. To name some of the most used libraries:

  • ajv
  • joi
  • v8n
  • validate.js

The validation library used for this article is ajv; nevertheless, all the conclusions also apply to the other libraries.

const Ajv = require('ajv');
const ajv = new Ajv();

const validate = ajv.compile({
  properties: {
    name: {
      type: 'string',
      minLength: 3,
    },
    age: { type: 'number' },
    pets: {
      type: 'array',
      items: {
        name: {
          type: 'string',
          minLength: 3,
        },
        legs: { type: 'number' },
      },
    },
  },
  required: ['name', 'pets'],
  type: 'object',
});

const getBiped = async () => {
  const data = await fetchData();
  console.log(data);
  // { name: 'John' }
  if (!validate(data)) {
    throw Error('Validation failed: ' + ajv.errorsText(validate.errors));
    // Error: Validation failed: data should have required property 'pets'
  }

  return data.pets.find(pet => pet.legs === 2);
};

Many validation libraries force you to define a schema wherein you can describe the structure to evaluate. Given that schema, you can create the validation function that will be used in your code.

The declaration of your schema will always depend on the library you are using; therefore, I always recommend checking the official docs. In the case of ajv, it forces you to declare in an object style, where each property has to provide its type. It’s also possible to set custom checkers for these values, like minLength for any array or string.

This method provides:

  • A standardized way to create validators and checks: The idea behind the schema is to have only one way to check for specific conditions inside your application — especially in JavaScript, where there are many ways to accomplish the same task, such as checking the length of an array. This quality is great to improve communication and collaboration inside a team
  • Improvement of error reporting: In case there is a mismatch on some property, the library will inform you of which one it is in a human-friendly way rather than just printing the stack trace

This new way of creating validations presents the following drawbacks:

  • Introduction of new syntax: When a team decides to add a new library, it becomes more difficult to understand the whole codebase. Every contributor has to know about the validator schema to understand how to make a change on it
  • Validators and types are not in sync: The definition of the schema and ExampleType is disconnected, which means that every time you make a change inside the ExampleType, you have to manually reflect it inside the schema. Depending on how many validators you have, this task can be quite tedious

One small comment regarding keeping validators and types in sync: some open-source projects address this issue, such as json-schema-to-typescript, which can generate a type definition from an existing schema. Then this won’t be considered a problem.

typescript javascript web-development developer

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Hire Dedicated eCommerce Web Developers | Top eCommerce Web Designers

Build your eCommerce project by hiring our expert eCommerce Website developers. Our Dedicated Web Designers develop powerful & robust website in a short span of time.

How long does it take to develop/build an app?

This article covers A-Z about the mobile and web app development process and answers your question on how long does it take to develop/build an app.

Best Web and App Development Company in Ahmedabad

Contact DataPierce for any questions or queries you have about website development, designing, marketing projects or any small or enterprise software development.