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
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
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 is assignable to
Bar, otherwise it is
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.