Learn TypeScript

Inferring types in a conditional type

Inferring types in a conditional type

In this lesson, we will learn how we can extract and use types within a condition type.

Understanding the infer keyword

There is an infer keyword that can be used within a condition in a conditional type to put the inferred type into a variable. That inferred variable can then be used within the conditional branches.

Consider the simple example below which gives the array element type if the type is an array:

type ArrayElementType<T> = T extends (infer E)[] ? E : T;
// type of item1 is `number`
type item1 = ArrayElementType<number[]>;
// type of item1 is `{name: string}`
type item2 = ArrayElementType<{ name: string }>;

When item1 is constructed, the condition in the conditional type is true because number[] matches (infer E)[]. E is therefore inferred to be number during this matching process. The first branch of the condition, E, is returned, which is resolved to be number.

When item2 is constructed, the condition in the conditional type is false because {name: string} does not match (infer E)[]. Therefore the second branch of the condition, T, is returned, which is the original parameter passed in, {name: string}.

An example

We are going to create a conditional type making use of the infer keyword in the code editor below:

TypeScriptOpen exercise in CodeSandbox

The code contains two functions that return different objects. The code also includes types for the two functions.

We will create a utility type called FunctionReturnType that will give the return type of a function. We will eventually create a union type consisting of the return types of the two functions using FunctionReturnType.

  • Let's start to create a type alias for FunctionReturnType:
type FunctionReturnType<T>
  • Add the condition for FunctionReturnType:
type FunctionReturnType<T> = T extends (...args: any) => infer R

Our condition is that the type passed in matches a function signature. We have inferred the return type of the function to be put in a R parameter.

  • Add the branches of logic to the conditional type:
type FunctionReturnType<T> = T extends (...args: any) => infer R ? R : T;

The R parameter is returned if the condition is true, which is the function's return type. The original type passed in, T, is returned if the condition is false.

  • Create a union type called Actions that consists of the return types of the addPerson and removePerson functions using FunctionReturnType.

Actions is equivalent to the type below:

{
type: string;
payload: string;
} | {
type: string;
payload: number;
}

Great!

  • Now try FunctionReturnType by passing in an object:
const person = { name: "Fred" };
type PersonType = FunctionReturnType<typeof person>;
🤔

What is the type of PersonType?.

It's nice that FunctionReturnType can handle non-function types correctly. It would be nicer if a type error was raised if we tried to pass in a non-function type.

🤔

How can we add a constraint to FunctionReturnType so that only functions can be passed to it?.

A type error is now raised on PersonType, which is great.

We have actually just created a type that already exists as a standard utility type called ReturnType. The definition of ReturnType is below:

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

This is similar to our implementation. A difference is in the second branch of the condition (T v any), which should never be reached because of the generic parameter constraint.

Summary

The infer keyword allows types to be extracted from conditions in conditional types. This is often used within Typescript's standard utility types.

In the next lesson, we will gain a deep understanding of some of TypeScripts standard conditional utility types.

Did you find this lesson useful?

Share this lesson on twitter
© 2024 Carl Rippon
Privacy Policy
This site uses cookies. Click here to find out more