Where to put input validation in Typescript?

You can leave Feedback and follow me on Twitter.

In a web application we wonder where to put validation logic. Often validation logic is not coordinated and put into several places. Javascript validation in the browser, input validation in the web or API controllers, input validation in the service layer and input validation at the database layer. This blog post proposes to use tag types to reduce input validation and clearly define where to put and handle it. Some of the benefits could be achieved by using custom types, but those increase lines of code and many developers don't like having hundreds of small classes. As an example language we use Typescript.

Our example application is for buying on the internet. We have a controller method with an additional parameter that specifies a rebate as a precentage. The percentage

  • should be an integer, we do not support 20.3%
  • should be between 0 and 100, instead of 0 and 1
  • should be below 80 in this case for a coupon, as we do not want to give 100% rebates

I've chosen this example because as a developer once I created a bug were due to bad documentation I assumed a percentage Double value was between 0 and 1 (as a mathematician would do) while the developer who wrote the code thought it to be between 0 and 100 (like a marketing person and perhaps some product managers would do).

Let's look at some code

We use a string parameter with the percentage of the coupon. First step is to validate it's a number. Then we call buy and decreaseInventory. The buy method could both return validation errors (not a percentage) or business errors like out of stock. We need to handle both error types here. If there are none, we can call decreaseInventory.

The buy method needs input validation to make sure the parameter given has the correct syntax, correct semantic and valid values. The same goes for the decreaseInventory method.

Exception handling in Javascript isn't very good. It adds boilerplate and has confusing execution flow. We replace exceptions with an error result in version 2 of our example. We also push input validation to the edges in the web framework by making the paramter a number instead of a string in the second example.

This has less boilerplate from the exceptions than the first example, but we have still validation logic inside our functions.

To correctly call the buy function we need to read the documentation about the format of percentage. After reading about percentage, as a good coder we do not push invalid data to a function, so we validate it first, adding more validation logic.

This leads to duplicated validation logic with all the problems of code duplication. More code leads to more bugs, changing code needs to be done in several places, forgetting one place leads to bugs. More code makes understanding and reading code more difficult.

Code duplication is due to the fact, that the validation we have done is forgotten with the number type. We validate it's constraints (0-100, smaller than 80, Integer) but then forget this validation so it needs to be done again in the service layer at buy and decreaseInventory. If we have a database layer, we need to validate there a third time.

With tag types we can 'store' these constraints. A tag type refines a type with syntactic or semantic constraints.

Here Positive is a constraint on the rparamater which is of type number. In Typescript tag type are based on Intersection types.

I've written a tag type library for Typescript called taghiro which makes creating tag types easy and already brings dozens of predefined and useful constraints. In Scala one can use refined. Our example rewritten with tag types from taghiro looks like this:

This code has many benefits. First we need to put validation logic only in one place. The service layer looks much cleaner without the validation logic. One can also directly see the parameter constraints from the method signature. percentage needs to be a number, follow the Percentage constraints and be less than 80.

Then validation error handling and business error handling are seperated in two places. Validation handling before we call a method, business error handling after we have called a method.

A developer is forced to do validation first, he can't push faulty data lazily into the service layer, preventing potential bugs. The compiler makes sure at compile time that the parameter fullfils the right constraints and validation logic is in place. The caller needs to ensure those constraints and the service layer is not required to validate the input from the caller.

A small side note: If you are familiar with Java, in the early days Java developers had to deal with three main errors. Null Pointer Exeptions (NPE), Class Cast Exceptions (CCE) and Illegal Argument Exceptions (IAE). NPEs have been mostly solved by Optional. CCEs have been solved by Generics. Tag types solve the third major error class, IAEs.

Using tag types reduces boilerplate, reduced code duplication, pushes validation to the edges and makes code more readable and easier to understand. taghiro is open source and MIT licensed.