How to Use Plain Functions to Validate Complex Structures

How to Use Plain Functions to Validate Complex Structures

Javascript has a few good libraries for data validation. If you’re using TypeScript, you probably will want a library that can not only validate your data, but also can generate static types automatically. There are a few libraries that aim at solving that problem, but the most popular right now are class-validator and io-ts.

Class-Validator, as the name implies, requires you to create a class for each interface you want to validate and use annotations as the property validator. It makes the code very nice and clean, but if you ever want to create your own validators things become less trivial.

IO-TS uses a different approach and lets you validate any type you choose without the limitation for classes or objects. The generated TypeScript interfaces are pretty robust, and the code is strongly aligned with TypeScript methodology for defined types. Creating new types is less user-friendly — it appears that many basic features like string length range are missing from the list of current types available.

There are many more solutions and libraries. I took a deep look into many of them, but I was not able to find a simple and elegant solution that satisfies my needs.


Introducing: Function Interface Validators

When I wrote computed-types I had only one goal in mind: Create a simple way to define runtime types that can also be used as TypeScript types.

In my research I came up with a simple and easy solution that has already been used extensively: function interfaces!

The idea is simple — to create a library that converts every function to a type validator. The function’s first parameter will be the type input, and the function return type will be the actual value type. If the function throws an error during execution, the input is considered invalid.

function ValidNumber(input: unknown): number {
  const value = Number(input);

  if (isNaN(value)) {
    throw new TypeError('Invalid number');
  }

  return value;
}

Given those function types, we can create a generator that takes a schema and returns a function that validates any input to match this schema.

import { Schema, Or, Type } from 'computed-types';

const UserSchema = {
  name: String,
  status: Or('active' as 'active', 'suspended' as 'suspended'),
};

const validator = Schema(UserSchema);
let user: Type<typeof UserSchema>;

TypeScript can automatically read the schema structure and alert me on compile time if I try to validate an invalid input:

try {
  user = validator({
    username: 'john1',

    // @ts-ignore Type "unregistered" is not assignable...
    status: 'unregistered',
  });
} catch (err) {
  console.error(err.message, err.paths);
}

As you can see, creating new validations or combining one validator with another is easy. Any validation function that already exists somewhere, e.g., email validator, can convert to a type validator in one line of code, making it easy to reuse existing code in your project.

import { Test } from 'funval';
import * as EmailValidator from 'email-validator';

const Email = Test(EmailValidator.validate, 'Invalid Email');

Conclusion

If you like this approach, I encourage you to check out the computed-types repository and learn more about all the features of this tiny library. Feel free to send new PRs or open an issue.

Thanks!

Enjoyed the read?

Don’t miss out on my next article! to stay updated on the latest insights.

Need advice on your startup?

I’m available for interesting web projects. Send me some details and let’s start working!

Hire me
avataravataravataravataravatar

Let’s stay in touch!

Join 50+ other developers who receive my thoughts, ideas, and favorite links.