Understanding the TypeScript `infer` keyword
In TypeScript, there are some types that are composed of multiple other types. We can refer to them as compound types. A promise for example, is a compound type because it’s composed of two types. The first being the Promise
itself, and the second being the type of value that the promise holds.
A function is another example of a compound type because it consists of three different types. The first being the function itself, the second being the type of arguments that the function accepts, and the third being the type of result that the function returns.
And as a final example, we also have the array, which is composed of two types: The array itself and the type of values that the array holds.
So what does this have to do with the infer
keyword? Well the infer
keyword is what we would use if we wanted to determine one of the types of a compound type. So for instance, if we wanted to determine the type of value that is held by a promise, or the type of result that a function returns, we could use the infer
keyword to do so.
Let's take the promise as an example and let's create a TypeScript utility that would extract the type of value of a given promise. Such a utility might look something like this:
type Unpromisify<P> = P extends Promise<infer V> ? V : never
Now let’s break it down. With the first part of the statement...
type Unpromisify<P> =
What we are saying is that the utility is called ‘Unpromisify’ and that it accepts an unknown type called P
, as an argument. This is similar to how we can have a function called Unpromisify
that accepts one argument called P
.
function Unpromisify(P) {...}
The difference, however, is that the typescript utility will accept a type
as an argument whereas the function would accept a value
as an argument.
Now with the second part of the statement…
= P extends Promise<infer V> ? V : never
What we are saying is that if P
is a Promise of some value whose type we will declare to be V
, then infer what V
is and return it to us.
You see, the statement...
P extends Promise<infer V> ? V : never
is a conditional statement that takes the form of
condition ? trueExpression : falseExpression
As in...
<If condition is true> ? <Then do this> : <otherwise do this>
In our case, for the condition to evaluate as true, P
must extend a Promise that holds a type of value that we will declare to be V
. Once this condition is true, then it should return the inferred type of V
, otherwise return the never
type or no type.
Think of V
as being somewhat like the X
in an algebraic equation. Something like saying...
If 7 is equal to 5 + X, then tell me what X is
We are somewhat asking the same thing with the infer
keyword.
If P extends Promise<infer V> ? then tell me what V is : otherwise tell me nothing
In this case, the infer
keyword is the thing that designates that V
is the equivalent of X
(i.e., that it's the thing that we need to deduce). P
we know because P
is an argument that we specified. But V
we don't yet know because we have not declared it anywhere. So by placing the infer
keyword in front on V
, what we are doing is declaring that V
is a container for an unknown type that we want to get a hold of. It's somewhat like declaring a variable within a function:
const Unpromisify = (P) =>
P === Promise<var V>
? V
: undefined
or
function Unpromisify = (P) {
var V
if(P instanceof Promise) {
V = extractPromiseValueOf(P)
}
return V
}
Now notice that in my description of the second half of the Unpromisify
utility, I said that what we are saying is that if P
is a Promise of some value whose type we will DECLARE to be V
, then infer what V
is and return it to us. Well my use of the term DECLARE
here is intentional because that is essentially what we are using the infer
keyword to do. It's a way of declaring a type variable so to speak, similar to declaring a variable in javascript.
It's only for conditionals
Now there is one main thing to know about the infer
keyword, and it's that just like the algebraic equation we saw above, the infer
keyword must be used within a conditional statement, as it’s the only way to get an expression to evaluate to the type being inferred.
Take the following statement as an example...
type Unpromisify<P> = Promise<infer V>
This statement is useless to us if the aim is to get the type of V
because the expression does not evaluate to V
. Instead it evaluates to a promise of V
. While we have declared V
for inference, we are not grabbing a hold of it. To get V
, we need an expression that can allow us to grab a hold of V
and return it. Conditionals allow this by giving us the opportunity to evaluate to V
either when the condition is true or when it’s false.
type Unpromisify<P> = P extends Promise<infer V> ? V : never
So in essence…
The infer keyword allows us to use a conditional statement to infer an unknown type.
or rather...
It's what we use to declare a type for inference within the body of a conditional statement
It's not restricted to compound types
Now at the beginning, I mentioned that we could use the infer
keyword to determine one of the types of a compound type. This might have given the impression that the infer
keyword could ONLY be used on compound types but that is not the case. The type that is being inferred doesn’t have to be part of a compound type. Compound types are just the most common use case and so it makes it easier to explain the intent and usage of the infer
keyword. We could also use the infer
keyword to infer on non compound types as well. The following is an example:
type Foo<P> = P extends infer V ? V : never
While it may seem redundant because it’s essentially the same as doing...
type Foo<P> = P
…it’s perfectly legal. With it, what we are saying is that if P
extends some type that we will declare to be V
, infer the type of V
and return it. And because P
will definitely extend something, the condition will evaluate to true, and so the type that P
extends is what will be returned.
type Bar = Foo<typeof 3>
// Bar will end up being a type of number
Some more examples
Inferring the return type of a function
So now that we have somewhat of a grasp of what the infer
keyword does, let’s look at some more examples of it. Let's say that we wanted to create a utility type that returns to us the return type of a given function, we could construct it as follows:
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never
Here we are saying that if F
is a type of function that returns some result that we will declare to be of type R
, then return R
.
Inferring the type of a functions arguments
Now let's say that we wanted to get hold of the type of arguments that the function accepts. A utility that does exactly this, would look something like the following:
type Parameters<F> = F extends (...args: infer P) => any ? P : never
With this, what we are saying is that if F
is a type of function that accepts arguments that we will declare to be of type P
, then return the type of P
.
Inferring on strings
Let's do something a little bit different and explore the realm of strings. Let's say we have a string that starts with a space character and we want to get the type of string that comes after the space... in other words, we want to be able to do this:
type S = StringAfterSpace<' Hello'>
// S should resolve to a type of 'Hello' without the space in front of it
We could construct the StringAfterSpace
utility as follows:
type SpaceChar = ' '
type StringAfterSpace<S> = S extends `${SpaceChar}${infer R}` ? R : never
Again, with this utility, what we are saying is that if S
is a type of string (that we will declare to be R
) that starts with a space character, then return R
.
So there you have it. This is the infer
keyword in a nutshell.