A Good Way How to Validate Types in TypeScript

TL;DR: I've created a typescript library fp-ts-type-check. You can use it to validate the structure of data you get from outside of your application.

Here's the situation: your typescript application needs to retrieve some object via some API. You make an API request and you get a successful response with some json. Probably it's the data you expect but you can't say for sure. Your actions?

The easiest way is to assume the data we've got in json is actually the data we expect and to do an unsafe type cast. But this sounds like giving up: we did all this work defining types within application just to accept an unknown value from outside and possibly get a "e is undefined" error again.

But we can do safe type casts by checking the input value before casting. TypeScript gives us type guards for this. You can write a function that returns a fake type x is MyThing which is actually a boolean but it tells compiler that it's safe to cast value x to a type Foo. The only problem with type guards is that this is also a valid code:

type MyThing = { foo: string; bar: number }

const isMyThing = (x: unknown): x is MyThing => true;

isMyThing('Not MyThing'); // Returns true

In this case isMyThing confirms anything is an instance of MyThing and TypeScript trusts you in this case. It's your responsibility to write boilerplate like this:

const isMyThing = (x: unknown): x is MyThing =>
    typeof x == "object" &&
    typeof (x as MyThing).foo == "string" &&
    typeof (x as MyThing).bar == "number"

Then it's your responsibility to test it and it's your responsibility to remember to update your type guard if your type is updated. Not cool.

It would be cool instead to make your complier to do the work on checking that your type guard matches the type you're trying to guard. And that's possible if we go one abstraction up and start treating our type guards as composable values:

type Guard<T> = (x: unknown) => x is T

Now let's create type guards for some primitive types like string and number:

const isString: Guard<string> = (x: unknown): x is string => typeof x == "string"
const isNumber: Guard<number> = (x: unknown): x is number => typeof x == "number"

And now a type guard that checks our value is an array of strings. But we don't want to write a specific array type guard for every possible type. Instead we'll write a higher kinded type guard: a function that accepts a type guard for any type and returns a type guard for array of items with this type:

const isArrayOf = <A>(itemGuard: Guard<A>): Guard<Array<A>> =>
  (x: unknown): x is A[] =>
    Array.isArray(x) && x.every(itemGuard)

type Foods = string[]

const foodsGuard: Guard<Foods> = isArrayOf(isString)

console.log(foodsGuard(["spam", "ham"])) // true
console.log(foodsGuard(["spam", 2])) // false

And now we can see an interesting thing: array type guard composes it's type from the type of item guard and it's just a coincidence that it's the type we use in out model. But this coincidence is validated by TypeScript compiler, if we provide wrong type guard or our model type changes, we'll have a compilation error.

const badGuard1: Guard<Foods> = isArrayOf(isNumber) // won't compile
const badGuard2: Guard<number[]> = isArrayOf(isString) // also won't compile

We can also compose type guards for records from type guards for every field. In this case it's easier not to derive final type from parameters but to use a mapped type for creating a record type config. Result is the same: everything will compile only if your type guard configuration matches your type.

// An object with same keys as in A but value types are Guards for original types
type PropertyGuards<A extends object> = { [K in keyof A]: Guard<A[K]> }

const isType = <A extends object>(propertyGuards: PropertyGuards<A>): Guard<A> =>
  (x: unknown): x is A =>
    /* Scary code for validating every property here */

const isMyThing = isType<MyThing>({ foo: isString, bar: isNumber })

And that's how we make a good type guard. The fact that we don't write a type guards for each specific type but instead compose them from existing ones relieves us from boilerplate and makes our code really type-safe.

This can be even better. If the type validation fails and it's a really big value we want to know what exactly failed. So instead of boolean we want to receive either a parsing error or a value safely casted to our expected type. Also it would be good if we had a lot of ways to unwrap or combine this result and is would be even better if we didn't have to write any new code to have all this. Fortunately, there is a type for this in the fp-ts library and it's called Either. Now type Guard<T> = (x: unknown) => x is T turns into type Parser<T> = (x: unknown) => Either<ParseError, T>.

And that's how you get the fp-ts-type-check – a library for composing type-save type checkers with some primitive checkers and a bunch of combinators to validate types of any complexity. So instead of copying all those code pieces from above you can add this library and get everything ready to use. Isn't that good?

Tags: , , , , ,