How to Validate JavaScript Object Better with TypeScript

Djoe Pramono
Oct 16, 2019 · 9 min read

So recently I came across a block of code, which sent me thinking for a whole night. The code worked just fine but something about it really bothered me and I was thinking, “Can it be made better?

image borrowed from TheHoot

The code is more of less like the following, where isAllowed is a network call to an external API.

So what bothered me?

  • Too many ifs
  • Errors are thrown one by one
  • Handles error via throwing exception

How to make things better

In my mind, why can’t we rewrite the code into something like this?

It shall return all validation errors or return the input as a valid result. And before some of you start screaming Promise.all, Nope! Simply usingPromise.all won’t achieve what I want here. But more on this later.

So I embarked on a journey to create an NPM module that can help validate JavaScript object better. Well actually not just object, but any JavaScript data types. Let’s refactor my example code near the top of this article into a better one and along the way we will find out that we actually extract the validation logic into a module.

By the way, I decided to use TypeScript to help me writing this module because it’s becoming main stream now and typed language helps in building a good system.

It shall return all validation errors or return the input as a valid result

Async Function and Promises

Before we start, let’s have a refresher on async functions and Promises

  • Async functions returns aPromise. But a normal function can return a Promise without being labeled as async.
  • Promise can be resolved/rejected with any value, or results in an Error exception
  • Async functions can be composed into a Promise chain
  • If you wait for async function with await, that means you are inside async function
  • Error exception and rejection break Promise chain at once
  • You can start an async function inside async function
  • Currently there’s a warning if you don’t handle promise rejection in Node and Chrome

I hope the dot points make sense. If not, don’t worry it will become clearer as we start unraveling how this validation module should work.

Type All the Things

Okay now let’s move on to the solution that I come to. First thing first, let’s put down all of the types that we need. Always start from the type first. TyDD, Type Driven Development 😜

The person data type is pretty straight forward.

Next up rather than throw Error (a.k.a. exception)out straight away, it’s better if we model the error properly and thus we can use them in pure functions. This way we also have full control on what to do with the errors.

Let’s state the obvious first, we choose the name Invalid here, because there is already a class namedError which represents the exception.

Now some of you might wonder why an object to represent an error instead of a string? Well it’s because in run time, JavaScript typing is very limited. If we set error type to string, how are we going to differentiate between an error and a valid string?

But why do we bother to create a class instead of just using the interface straight? Because class have a better built in checking for the Type Guard. Type Guard is basically a way to check the type on run time. But more on this later.

Always start from the type first. TyDD, Type Driven Development.

Let’s move on to typing the validation function.

In order to make a function pure, it needs to have a return value. Here we use InvalidOr<T> as the return type which is a union type of either Invalid or T. T here is a generic, it’s the type of the object that we are validating. It can be a string, array, or even an object. In our case, this is Person. So our function can be written as follows

or alternatively just type the function instead of the parameters

Unfortunately the latter might cause a linting error in you are using typescript-eslint default setting

Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)

To remedy that just put the return type on the function.

Type Guard

All right now back to Type Guard. As mentioned earlier, it’s a way to check types on run time. It’s basically a function.

isInvalid here checks if the input object has the errorMessage property defined. If it is true than the input object is considered as Invalid

This is why having Invalid as a class is better, because we can use instanceof as the Type Guard. Which is stricter than just checking the property. See the comparison below

It doesn’t stop there though, TypeScript is smart enough to narrow the type if there’s an if else statement using the Type Guard

Eventhough technically result is of type InvalidOr<Person>, on the first block of if, it has been narrowed down to Invalid because we are using the Type Guard. Subsequently, it is also narrowed down to Personin the else block.

Try changing the if statement from if(isInvalid(result)) to if(result instanceof Invalid), you will be hit with a compilation error. Both are similar but different during compilation time. Mainly because TypeScript type system is not as strong as other ML Language e.g. Haskell. It is after all built on top of a dynamic language. This is why in the Type Guard we have errorOr is Invalid, to signify that this function works on the Invalid sub type of Invalid | T

Running Validations Synchronously

As if it’s not already obvious by now, we need to separate the validation logic into functions that handles one kind of validation each i.e. isAllowed, eighteenOrAbove, and nonEmptyName. For simplicity sake, let’s ignore the async function isAllowed for now. And let’s look at how we can code the implementation of running the synchronous validations.

It should be pretty straight forward. Remember that we want to create a function that

  • takes an array of functions that accept same input (a.k.a Validate functions)
  • also take an input object to be validated
  • and it return either an array of Invalid or the original input object.

Welcome to ValidateAll which returns Validated<T>

There’s a little surprise there, NonEmptyArray. It’s basically a type that guarantees that the array is non empty during compilation. It’s a custom type made possible by TypeScript 3.0 rest element in tuple types

All right, now let’s jump to the runner implementation

It’s actually pretty straight forward

  • mapover the functions and execute them with the input object.
  • make sure that we catch the Error exception and put it inside Invalid because we want to collate the result
  • check the results and group the Invalid into an array with reduce
  • if errors found is greater than zero, return an array of Invalid otherwise return the original object. Note that this needs to be done via Type Guard, otherwise the TypeScript compiler might complain.

And an example on how to use the runValidations would be something like this

Types for Asynchronous Validation

The asynchronous version is not that much different. Let’s start with the types

As you see AsyncValidate<T> is similar to Validate<T> and AsyncValidateAll is similar to ValidateAll. The only difference here, is the result is wrapped in a Promise. Because remember async function returns Promise.

If we want to run all validations, it doesn’t matter if it is synchronous or asynchronous, and evaluate all results. We would need to turn all of the synchronous validations into asynchronous. This is quite straightforward, just add async and wrap the return type in Promise

We then put all of those asynchronous functions into an array and pass it into AsyncValidateAll so that we can run the validations in parallel.

Running Validations Asynchronously

How does the asynchronous runner look like?

It’s slightly more complicated than the synchronous version

  • Similar to the runValidations, we map over the functions and execute them with the input object
  • At the core, we are usingPromise.all to asynchronously get the value out of the validation promises
  • However before passing it to Promise.all, we need to append each Promises to have catch and turn the catched errors into Invalid
  • The rest are again similar to runValidations, group the validation results via reduce and use areInvalid Type Guard to determine what to return

We extract the function that is responsible to turn catched errors into Invalid because it’s rather special. The catched e actually has any type, mainly because Promise.reject can reject with anything e.g. string, object, etc. Thus we need to handle this carefully like so

An example of how to use the runAsyncvalidations would be something like the following

Why can’t we just use Promise.all ?

Promise.all, on success will return an array of resolved Promise. And on errors, it will return an array of Errors

It looks almost perfect, except that on Promise rejection,Promise.all will only return the first rejected promise. So assuming it errors on eighteenOrAbove, succeed on nonEmptyName, and rejects on IsAllowed, the result would be the return value of rejected IsAllowed and that’s it. This is not what we want.

Why can’t we just chain the validations?

The problem with the above is that any exception came out from the validation function and any Promise rejection results in early termination of the Promise chain. Which mean we don’t collate all of the validation errors

Epilogue

And there we have it, we have written a validation module in TypeScript 😃. This module is available at npmjs under the name falidator. I have just recently released it for beta. If you are interested, please download it and have a play. Any feedback is appreciated, and as always thank you for reading!

More From Medium

Related reads

Also tagged Functional Programming

Also tagged Functional Programming

JavaScript Function Construction (Part 3)

100

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade