A Better Way to Validate Data in TypeScript

Data validation tends to get out of hand and become difficult to maintain and understand. In this article, we explore a better way by using our open-source library

Shlomi Borovitz
Altostra
5 min readMay 18, 2020

--

The Problem at Hand

It’s a fact that user input cannot be trusted and must always be validated. Yet, code like in the example below is too common. Although seemingly it returns the type we expect, in reality, it’s just lying to the type system and to ourselves!

We do not know what the body contains, and while wishing it to be of a specific type — we do not know this to be true.

Moreover, unlike static languages, JS does not have type-aware deserializers that validate correctness and validity (not even at the type level).

And, while JSON schema does allow us to validate inputs, it is difficult to understand and does not translate intuitively to TypeScript types. In complex scenarios, it is difficult to tell what is considered legitimate input and what is not. It is also difficult to figure out why a certain value did not pass one of the schema validations.

Usually, APIs that parse JSON and input would return the any type, on the false assumption that the developer knows what the input is — but when dealing with user-input, the developer does not (though we usually assume we do).

Example: The lunch delivery store

Suppose we are having an online service that allows its customers to place orders for their lunch meal: a pizza or a hamburger.

Clients may POST an Order:

And our handler function should parse an order and process it:

As mentioned before, assuming the request body is an Order object is a mistake. A mistake that a hacker may exploit and might cause unforeseen consequences.

Using Type-Guards

TypeScript’s solution to runtime checking for the shape of a value is the user-defined type-guard: A boolean function which tells the type-checker what types a value can have in its true branch, and in its false branch.

While type-guards can bridge the gap between runtime type-checking and compile-time type-checking — it is on the developer to implement the runtime check, and thus, may require repeated code, that may or may not validate the input rigorously:

While this approach is much better than the “I trust all my users blindfolded” approach — it demands long and repetitive code, that in the end is not necessarily trustworthy.

Better Type-Guards with @altostra/type-validations

Neither JavaScript nor TypeScript makes it easy to validate user input (beyond the simplest examples), TypeScript makes it possible to have compile-time type-checking that is bound to runtime checks. We published an open-source library on NPM@altostra/type-validations that helps you easily compose complex data validators.

Let’s rewrite our type-guard from the previous example, but this time using the type-validations library:

Now, the unknown request body is inferred correctly to be an Order type:

Inferred type for `order`

The inferred type of order variable is not exactly the Order type, but the type we actually tested for.

Coding an Order

Having the tools to describe types efficiently, we can now add the Order properties types, starting with the simplest and the best: Topping.

The Topping type is just an object with two string properties:

While we can use the generic primitives.string type-validation for the type and cover properties, there is only a subset of valid string values these two properties can accept.

Having the Toppings, we can add the Pizza:

Then the Hamburger in a similar way:

And having all the food on the menu, we can finally code the rest of our order:

Is the input-validation rigorous? Well, it is much more rigorous than our first try— but we can do better.

For example, quantity must be a positive integer. After all, NaN is a number but not quantity.

And this time, order is rightfully inferred to be an Order:

Inferred Order type

Handling Rejections

Now that we have our web-server, we can validate all user-input without assuming anything about it.

Later, our enthusiast mobile developer implemented a frontend application, and for a while, everything was well. But only for a while. After an update, more and more users are reporting that they’re failing to place an order. The logs are showing an increased number of 400 status code being returned to clients, saying "Invalid order". But why?

If the update included a small change, it could be reverted, or for the very least, inspected. But what if the update was a big refactor or a new version? Fortunately, all type-guards created by @altostra/type-validations can take a second argument — a function that would be called (possibly multiple times) if a value fails validation — with a reason for the rejection.

Can you spot the reason for the error?

From the validation output below, we can see that the value 'chilli' does not equal to any of the toppings. Careful inspection reveals that 'chilli' does not equal to 'chili', and that of course is the cause of the problem.

Using the path property, we can find where the offending property is, and trace its source.

The output of invalid-order rejections

And selling back (buying a negative quantity of) hamburgers will not work either:

Sell hamburgers back to the delivery store

Combining Everything with Assertions

Having backends with many endpoints can lead to repetition in both collection and logging rejections, and handling the invalid value itself (usually by throwing an error).

On the other hand, since TypeScript 3.7, control-flow analysis can use the so-called “assertion functions”, which assumed to return (and not throw) only if the type of the parameter is known.

Given that there is a general way to handle validation errors (e.g. log the rejections using your preferred logger, and throw BadRequest error), it could be encapsulated into a function.

We can then use that function to create per-type assertion, and use it to clean even more our code.

In Conclusion

We’ve seen how by using @altostra/type-validations we create clean and clear code and yet it validates its input rigorously. If you feel that something is missing or can be improved, feel free to open issues and pull requests.

You can find the source code here https://github.com/altostra/type-validations, and on NPM https://www.npmjs.com/package/@altostra/type-validations

All code snippets from the post can be found here https://github.com/altostra/lunch-delivery-store

--

--