Removing boilerplate with Typescript

Mark Jordan
Nov 12, 2019 · 3 min read

Let’s say we have an api object we want to inject into our code. We also want to test this code, so we need to define the interface for our api object so that we can create appropriately-shaped mock objects. We want the compiler to check that our mock object at least implements the same methods as the real api.

Coming from C#, my first attempt at this looks like a traditional interface with two different classes to implement it:

We don’t actually need a class here, though, so we can eliminate a bit of boilerplate syntax with an object literal and a plain factory function:

There’s a much bigger change we can make, though. Currently adding a new api function means parallel changes in both the interface and the createApi methods. Using infer, we can tell typescript that the Api type is just whatever createApi returns:

There’s a lot happening on that first line, so let’s unpack it a little. We take a reference to createApi, and change the value to a type with typeof createApi. Even though we haven’t actually specified createApi‘s type anywhere, typescript can infer what it is by looking at the parameters it takes and the object it returns. We only care about the return type of the function, though, so we can extract the return type using ReturnType<T> and save it as a type called Api.

Here’s a (slightly simplified) definition of ReturnType<T> , so we can see how it works:

type ReturnType<T>= T extends (...args: any) => infer R ? R : never;

This is a lot to unpack, so let’s take it step by step.

The first thing to notice is the extends keyword, which means we’re using conditional types. That means we can declare something like this:

type ConditionalType<T> = T extends Foo ? Bar : Baz;

Here we’re saying that the type ConditionalType<T> is different depending on the type T. If T is assignable to Foo, then ConditionalType<T> is Bar, otherwise it is Baz.

The second thing is what we’re checking for: T extends (args) => infer R. We’re checking whether T is a function or not. Instead of specifying the return type we want, we’re saying infer R. The infer keyword puts a hole in the type definition and asks typescript to fill it in for us.

Finally, we say that if the type matches then the result of the conditional type is R — the return type that we inferred. If the type doesn’t match (ie, T isn’t a function) then the result is never a special type that can never be instantiated.

The upshot of all this type magic is that we no longer need to keep a separate interface and implementation in sync for any code which depends on Api. As soon as createApi returns a new property on the api object, typescript will verify that MockApi matches the new type and ensure that our tests keep behaving. We’ve gained a whole lot of type checking power with very little boilerplate compared to the native JS code.

Ingeniously Simple

Mark Jordan

Written by

Ingeniously Simple

How Redgate build ingeniously simple products, from inception to delivery.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade