Understanding the infer keyword in TypeScript
The infer keyword was always confusing to me, despite reading up on it multiple times. I’ll try to explain it clearly here.
Theinfer keyword can only be used in conditional types in order to declaratively introduce a new generic type variable. This is used for many default TypeScript utility types such as Parameters<Type> to get the parameter types of a function and ReturnType<Type>to get the return types of a function.
For example, the below type checks if T is a function, and returns the return type of that function.
type GetReturnType<T> = T extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>;
// ^ type number
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// ^ type boolean[]
In other words, we are able to declare the type of the return type of the function without explicitly defining a generic type variable.
Instead of forcing the return type of the function to be a certain type, we are asking TypeScript to define it as a type variable with the name Return which allows us to use it in other places.
What this solves
Some of you might think, why not just declare a generic type for the return value then? For example:
type GetReturnType<T, Return> = T extends (...args: never[]) => Return
? Return
: never;
type Num = GetReturnType<() => number, number>;
// ^ type number ^ if we don't fill in this we get an error
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[], boolean[]>;
// ^ type boolean[] ^ if we don't fill in this we get an error
However, this means that you have to define another generic type for the return value, which defeats the purpose of the type definition.
What about this?
How about not having the infer keyword at all? Why is it necessary?
type GetReturnType<T> = T extends (...args: never[]) => Return
? Return
: never;
// error, cannot find name 'Return'
This doesn’t work because we need to declare our type variables explicitly. TypeScript doesn’t know where the type variable Return comes from.
Basically, we can think of it as declaring a name for the type, with the type value being the actual type at that location.
More Examples
I was pretty confused when I first tried to understand this, so here are more examples for understanding.
Flatten
The below type returns the inner elements if it is an array, otherwise it returns the type itself:
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Num = Flatten<number[]>
// ^ type number
type OriginalNumber = Flatten<number>
// ^ type number
type Bool = Flatten<boolean>
// ^ type boolean
It basically declares the elements of an array as a type, and returns it if it exists. If not, it returns the T that does not extend it.
GetParameterType
The below type gets the parameters of a function type. I took it from the actual Paramter<T> type implementation from their utility types and made it simpler for better understanding.
type GetParameterType<T> = T extends (...args: infer Param) => any
? Param
: never;
type Num = GetParameterType<(n: number=> number>;
// ^ number
type Bool = GetParameterType<(n: boolean=> boolean[]>;
// ^ boolean
The type of the parameters of the function are declared as the type Param using the infer keyword.
Small Things to Note
There are certain things to take note which makes sense if you think about it:
- The inferred value can only be used in the true branch, because in the case where T does not extend the type, the inferred value would not exist.
type Flatten<T> = T extends Array<infer Item> ? Item : T; // this works
type Flatten<T> = T extends Array<infer Item> ? T : Item; // cannot find name 'Item'.
2. infer is always used within the extends clause and not anywhere else.
Summary
The infer keyword allows us to define types in the context of conditional types so that we can use them elsewhere. It is used extensively in utility types and are very powerful.
References
Documentation - Conditional Types