How to Validate JavaScript Object Better with TypeScript
--
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?”
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
if
s - 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 a
Promise
. But a normal function can return aPromise
without being labeled asasync
. Promise
can be resolved/rejected with any value, or results in anError
exception- Async functions can be composed into a Promise chain
- If you wait for
async
function withawait
, that means you are insideasync
function Error
exception and rejection break Promise chain at once- You can start an
async
function insideasync
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 Person
in 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
map
over 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 withreduce
- 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
, wemap
over the functions and execute them with the input object - At the core, we are using
Promise.all
to asynchronously get the value out of the validation promises - However before passing it to
Promise.all
, we need to append eachPromises
to have catch and turn the catched errors intoInvalid
- The rest are again similar to
runValidations
, group the validation results viareduce
and useareInvalid
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!