Understanding the Trickiest Concepts of TypeScript. A Deep Dive into Infer

In TypeScript, there are concepts and ideas that are quite difficult to understand, especially for beginner developers. One such concept is the keyword infer. Today, I propose to examine infer under a microscope and finally understand what it is used for and how you can start using it in your advanced types, even if you are just starting your journey in TypeScript.

Let's start with the fact that infer can only be used in conditional types and no other way. Infer helps create meta-types based on existing ones, applying pattern matching.

While everything sounds more complicated than it actually is, let's move on to examples, and everything will become much clearer.


Sometimes you need to get the type of a value returned by a function, for example, for typing a variable or parameter of another function. This may be necessary when we use library functions but do not have access to the type returned by that function.

type UserObject = {
	name: string
	age: number
}

export function constructUserObject(name: string, age: number): UserObject {
	return {name, age};
}

If you want to use the UserObject type further in the business logic, you need to either create the same type yourself or use infer with the following meta-type:

type GetFunctionReturnType<T> = T extends (..args: any[]) => infer U ? U : never

This literally means the following: a generic type is declared, which takes any type as input, and if this type is a function, it is saved in the variable U and returned, otherwise never is returned.

You can use it like this:

type ConstructedUser = GetFunctionReturnType<typeof constructUserObject>

Since the type of the constructUserObject function satisfies the condition, the UserObject type will be saved in the variable U and stored in the ConstructedUser type.

Those of you who are familiar with TypeScript and its utility types may object to the complexity, as we could use the built-in utility type ReturnType and you would be right. But if you look at the source code of ReturnType, you will find the following:

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

As you can see, it is almost the same helper that we wrote with a slightly stricter signature.


Alright, let's try to find a couple more examples of using infer.

For example, in React, there is a utility type ComponentProps, which allows you to get the type of props for an arbitrary component. Here is its simplified source code:

type ComponentProps<T extends JSXElementConstructor<any>> =
	T extends JSXElementConstructor<infer P>
		? P
		: {}

And I will also provide the code for the JSXElementConstructor type for clarity:

type JSXElementConstructor<P> =
	| ((props: P) => ReactElement<any, any> | null)
	| (new (props: P) => Component<P, any>);

Let's figure out what's happening here. The generic type ComponentProps takes any type that extends JSXElementConstructor, which in turn is a description of the type of a functional or class component in React.

Next, we check in the condition whether the extension is actually happening, and if so, we save the props type of this component in the variable P and return it, or return the type of an empty object.

In essence, we want to get the type of the parameter object with which the function should be called or the class instance created. We can also easily write such a utility-type:

type GetFnObjectParam<T extends {}> = T extends (objArg: infer U) => unknown ? U : never

I think you can already see for yourself what's happening here, no need to repeat.

We will use it like this:

declare function sum(arg: {one: number, two: number}): number;

type SumParam = GetFnObjectParam<typeof sum> // {one: number, two: number}

And finally, let's analyze a more complex example. In the source code of the redux-toolkit project, a well-known starter kit for redux developers, there is the following helper:

type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never

This is a frequently encountered utility-type for converting a union to an intersection, which can be found not only in redux-toolkit but also in a huge number of other projects. With its help, this:

type Union = 'A' | 'B' | 'C'

Could become:

type Intersection = 'A' & 'B' & 'C'

Of course, UnionToIntersection will work not only with string constants, as in my example, but also with complex types:

type Type = UnionToIntersection<{one: number} | {two: number}> // now Type is {one: number} & {two: number}

Looking at the UnionToIntersection code, it may be completely unclear what is happening. Let's break it down step by step:

  1. Declare a generic type
  2. Then, within the first expression in parentheses, we check whether the type passed to the generic extends any. Almost all types in TypeScript extend any, so the result of the expression in parentheses will be a function that takes the passed type as a parameter
  3. Next, there is another conditional type that checks whether the type - the result of the expression in parentheses - extends a function with a single parameter. We know that it does because this is the type that resulted from the expression
  4. Using pattern matching with infer, we capture the type of the function parameter and return it

All of this is good, but why should it even work, since the resulting type simply returns the same type that was passed as an argument to the generic? Why does this magic of running the type through the function parameter change the union to an intersection?

Indeed, there is a certain amount of TypeScript magic involved, and three concepts are at work here, the first of which is Naked type.

If the type being tested in the condition is not wrapped in anything, it is called naked:

type Naked<T> = T extends any ? (x: T) => any : never // T is not wrapped in anything therefore T is naked

type NotNaked<T> = T[] extends any ? (x: T) => any : never // T wrapped into an array so not naked

type NotNaked<T> = {key: T} extends any ? (x: T) => any : never // T wrapped into an object and also not naked

In the case when our type is naked, the TypeScript mechanism called Distributive conditional types comes into play. It works in such a way that if a union is passed to the conditional type, the conditional type is triggered for each member of the union, meaning the conditional type of the union becomes the union of the conditional types.

In our case, it looks like this:

type One = {one: number}
type Two = {two: number}
type Three = {three: number}

type Type = Naked<One | Two | Three>

//Becomes
type Type = Naked<One> | Naked<Two> | Naked<Three>

//Becomes
type Type = ((x: One) => any) | ((x: Two) => any) | ((x: Three) => any)

So, we have converted the passed union into a union of functions.

If we had run the union through the first NotNaked type, we would have gotten something like this:

type Type = NotNaked<One | Two | Three>

//Becomes
type Type = (x: One | Two | Three) => any

The difference is obvious, as they say.

Now, when we have a union of functions, the third most important concept in this case comes into play: contravariance of types through the function parameter.

In general, the concepts of covariance, contravariance, bivariance, and invariance deserve a separate, extensive article, and I will come back to that, but today there simply won't be room for a detailed explanation.

Now, let's create a variable of the Type type and assign it an arbitrary function:

type Type = ((x: One) => any) | ((x: Two) => any) | ((x: Three) => any)

const typeFn: Type = () => {}

The only possible argument with which we can call such a function will be an object that is a subtype of all supertypes One, Two, and Three simultaneously, containing all 3 properties:

typeFn({one: 1}) //Error, missing two и three
typeFn({one: 1, two: 2}) //Error, missing three
typeFn({one: 1, two: 2, three: 3}) //Works

And how can the last argument be described using a type? Like this:

const intersection: One & Two & Three = {one: 1, two: 2, three: 3}

This is exactly what UnionToIntersection does. Due to the contravariance of function parameters, the union is transformed into an intersection.

Alright, now that we understand how it works, let's find an example of how we can use UnionToIntersection in practice.

Often, we need to merge all object arguments passed to a function into a single object:

function mergeObjects<T extends object[]>(...objects: T): object {
	return objects.reduce((merged, current) => ({ ...merged, ...current }), {})
}

const obj1 = { a: 1, b: 2 }
const obj2 = { b: 3, c: 4 }
const obj3 = { d: 5 }

const mergedObjects = mergeObjects(obj1, obj2, obj3) // object

console.log(mergedObjects.a) //Error. Property 'a' does not exist on type 'object'.

In this case, the type of mergedObject will be an object that does not provide access to nested properties, even if we are sure that they are in place.

Can we improve the typing of mergeObjects? Yes, we can, and UnionToIntersection will help us with that. The improved version will look like this:

function mergeObjects<T extends object[]>(
  ...objects: T
): UnionToIntersection<T[number]> {
  return objects.reduce(
    (merged, current) => ({ ...merged, ...current }), {}
	) as UnionToIntersection<T[number]>;
}

const obj1 = { a: 1, b: 2 }
const obj2 = { b: 3, c: 4 }
const obj3 = { d: 5 }

const merged = mergeObjects(obj1, obj2, obj3) // { a: number, b: number } & { b: number, c: number } & { d: number }

console.log(merged.a) // Works

Now TypeScript knows the exact type of the merged object.


In conclusion, the TypeScript infer keyword is a powerful and flexible feature that allows developers to infer and manipulate types within the context of conditional and mapped types. It plays a crucial role in creating advanced type utilities and makes it possible to build more expressive and type-safe applications with TypeScript.

By understanding how to use infer, you can create more maintainable, robust, and type-aware code, significantly reducing the likelihood of runtime errors and improving the overall quality of their applications.

I will see you in the next article!