TypeScript’s infer shouldn’t scare you!

Vincent Francois
inato
Published in
5 min readDec 7, 2022
Photo by aldi sigun on Unsplash

When I first started using TypeScript, the infer keyword was the scariest of all. Stumbling on it usually meant closing the file and looking somewhere else.

With a little more practice and knowledge of TypeScript, I now believe infer is not that hard to understand. In this article, I’ll try to guide you to an understanding of what it can do and how to use it.

You’ll need some basic TypeScript knowledge to go through this. If it’s not the case, I highly recommend you go through this beginner’s tutorial before continuing (or here for the interactive version).

The issue

Let’s suppose we have a function that returns a Map:

const functionReturningMap = () =>
new Map([
[1, { foo: 'bar' }],
[2, { foo: 'other' }],
]);

// the type of this function is:
// () => Map<number, { foo: string; }>

Think of it as a function that comes from a library from which we don’t have much TypeScript information except the signature of the function.

I now would like, for each value of the map, to log something if the foo property is 'bar'. Here’s an implementation without a proper typing:

const logIfFooBar = (valueOfMap: any) => {
if (valueOfMap.foo === 'bar') {
console.log('is foobar');
}
};

functionReturningMap().forEach(logIfFooBar);

Can we get rid of the ugly any and retrieve the type of the Map’s value from the function’s signature? This would make the implementation of logIfFooBar safer and allow for auto-completion help when building it.

First step: retrieving the type of the function’s result

TypeScript has some built-in type helpers. You probably know some of them like Partial, Readonly, Array

Here, we are going to use ReturnType. Given a function as a type argument, this helper returns the type of the function’s result:

type FunctionReturningMapResult = ReturnType<typeof functionReturningMap>;
// Map<number, { foo: string; }>

Now, what remains is to extract the type of the Map’s value.

Second step: using infer to extract the type of a Map’s value

Let’s try to build a type helper like ReturnType that returns the value’s type of a Map: type ValueOfMap<T> = ....
This type uses a generic (the <T> part) If you are not familiar with generics, here’s another tutorial. Going through the folders 1 and 2 should bring you enough knowledge for our use of generics here.

The first thing we want to do here is check if T is a Map or not. Let’s use the extends keyword for that and just return true if T is a Map and false if not:

type ValueOfMap<T> = T extends Map<unknown, unknown> ? true : false;

type IsMap = ValueOfMap<Map<string, number>>
// -> true

type IsNotMap = ValueOfMap<Record<string, number>>
// -> false

We just used the extends keyword in what looks like some sort of ternary.
This is the way to write type-level conditions in TypeScript. Read it as:

If T is the type of a Map with whatever type for the keys and whatever type for the value, then return true, else return false

Now that we are able to tell that T is a Map, we’d like TypeScript to have a look “inside” the Map to retrieve the type of the value. This is exactly was infer is for:

type ValueOfMap<T> = T extends Map<unknown, infer Value> ? Value : never;

type NumberType = ValueOfMap<Map<string, number>>
// -> number

type ObjectType = ValueOfMap<Map<string, { foo: string }>>;
// -> { foo: string }

type IsNotMap = ValueOfMap<Record<string, number>>
// -> never

Read this as:

If T is the type of a Map with whatever type for the keys and whatever type for the value, take a look at the Map’s value type, extract it and return it, else return never

never is a type that can not be instantiated. No runtime value can ever be of this type. If a variable ends up having this type, you won’t be able to use it anywhere, except where using any but you should not be using any unless you really know what you are doing (in that case, you probably are not here reading this article)
Using never here is just making sure that we use our helper with a Map. If you use it with anything else, you’ll very soon reach a dead-end.

Wrapping everything up

Now that we have our type helper, let’s use it to type our function’s argument:

type ValueOfMap<T> = T extends Map<unknown, infer Value> ? Value : never;

const functionReturningMap = () =>
new Map([
[1, { foo: 'bar' }],
[2, { foo: 'other' }],
]);

type FunctionReturningMapResult = ReturnType<typeof functionReturningMap>;

const logIfIsFooBar = (valueOfMap: ValueOfMap<FunctionReturningMapResult>) => {
if (valueOfMap.foo === 'bar') {
console.log('is foobar');
}
};

And that’s it! Thanks to infer we were able to get the Map’s value type directly from our function’s type instead of writing the type ourselves.

To be honest, it would have been easier to type the argument directly with { foo: string } but I kept this example simple to demo the use of infer. We have defined this ValueOfMap helper in our codebase at Inato and it proved to be useful a few times.

Going further

There’s a small improvement we can do to our helper. Instead of being able to use it with something that is not a Map, can we make TypeScript fail when that’s the case? Yes! We can do that by adding a constraint on the generic of the helper:

type ValueOfMap<T extends Map<unknown, unknown>> = T extends Map<
unknown,
infer Value
>
? Value
: never;

type NumberType = ValueOfMap<Map<string, number>>
// -> number

type IsNotMap = ValueOfMap<Record<string, number>>
// -> TypeScript error:
// Type 'Record<string, number>' does not satisfy the constraint 'Map<unknown, unknown>'

Typecript won’t even compile in that case. This is better than having just a never type because the error is located where the issue is. Without it, we would have an error where the variable is used, which can be far from its declaration.

Notice that we have now two uses of extends:

  • the first one is used to limit the types with which our helper can be used
  • the second is used to allow the use of infer. infer can only be used in an extends condition. Think of it as “if something matches this shape, then infer this part of the shape for me, please”

The two serve different purposes and we can’t get rid of the second, even though the generic constraint means we will never be able to reach the never branch of the condition without TypeScript failing to compile. This is how infer works. I insist on this:

infer can only be used in an extends condition.

I hope you’ve learned a few things in this article and, most important, that infer is now as scary to you as the monster in this article’s picture!
Feel free to ask your questions in the comments!

--

--