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
- 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 using
Promise.all won’t achieve what I want here. But more on this later.
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 a
Promisewithout being labeled as
Promisecan be resolved/rejected with any value, or results in an
- Async functions can be composed into a Promise chain
- If you wait for
await, that means you are inside
Errorexception and rejection break Promise chain at once
- You can start an
- 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 named
Error which represents the exception.
Now some of you might wonder why an object to represent an error instead of a
string, how are we going to differentiate between an error and a valid
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
T here is a generic, it’s the type of the object that we are validating. It can be a
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.
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
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
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
Try changing the
if statement from
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.
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
- also take an input object to be validated
- and it return either an array of
Invalidor the original input object.
ValidateAll which returns
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
Invalidinto an array with
- if errors found is greater than zero, return an array of
Invalidotherwise 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
AsyncValidateAll is similar to
ValidateAll. The only difference here, is the result is wrapped in a
Promise. Because remember
async function returns
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
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
mapover the functions and execute them with the input object
- At the core, we are using
Promise.allto asynchronously get the value out of the validation promises
- However before passing it to
Promise.all, we need to append each
Promisesto have catch and turn the catched errors into
- The rest are again similar to
runValidations, group the validation results via
areInvalidType 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, on success will return an array of resolved
Promise. And on errors, it will return an array of
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
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!