DEV Community

Cover image for Understanding infer in TypeScript
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Understanding infer in TypeScript

Written by Simohamed Marhraoui ✏️

We’ve all been in situations where we used a library that had been typed sparingly. Take the following third-party function, for example:

function describePerson(person: {
  name: string;
  age: number;
  hobbies: [string, string]; // tuple
}) {
  return `${person.name} is ${person.age} years old and love ${person.hobbies.join(" and  ")}.`;
}
Enter fullscreen mode Exit fullscreen mode

If the library doesn’t provide a standalone type for the person argument of describePerson, defining a variable beforehand as the person argument would not be inferred correctly by TypeScript.

const alex = {
  name: 'Alex',
  age: 20,
  hobbies: ['walking', 'cooking'] // type string[] != [string, string]
}

describePerson(alex) /* Type string[] is not assignable to type [string, string] */
Enter fullscreen mode Exit fullscreen mode

TypeScript will infer the type of alex as { name: string; age: number; hobbies: string[] } and will not permit its use as an argument for describePerson.

And, even if it did, it would be nice to have type checking on the alex object itself to have proper autocompletion. We can easily achieve this, thanks to the infer keyword in TypeScript.

const alex: GetFirstArgumentOfAnyFunction<typeof describePerson> = {
  name: "Alex",
  age: 20,
  hobbies: ["walking", "cooking"],
};

describePerson(alex); /* No TypeScript errors */ 
Enter fullscreen mode Exit fullscreen mode

The infer keyword and conditional typing in TypeScript allows us to take a type and isolate any piece of it for later use.

The no-value never type

In TypeScript, never is treated as the “no value” type. You will often see it being used as a dead-end type. A union type like string | never in TypeScript will evaluate to string, discarding never.

To understand that, you can think of string and never as mathematical sets where string is a set that holds all string values, and never is a set that holds no value (∅ set). The union of such two sets is obviously the former alone.

By contrast, the union string | any evaluates to any. Again, you can think of this as a union between the string set and the universal set (U) that holds all sets, which, to no one’s surprise, evaluates to itself.

This explains why never is used as an escape hatch because, combined with other types, it will disappear.

Using conditional types in TypeScript

Conditional types modify a type based on whether or not it satisfies a certain constraint. It works similarly to ternaries in JavaScript.

The extends keyword

In TypeScript, constraints are expressed using the extends keyword. T extends K means that it’s safe to assume that a value of type T is also of type K, e.g., 0 extends number because var zero: number = 0 is type-safe.

Thus, we can have a generic that checks whether a constraint is met, and return different types.

StringFromType returns a literal string based on the primitive type it receives:

type StringFromType<T> = T extends string ? 'string' : never

type lorem = StringFromType<'lorem ipsum'> // 'string'
type ten = StringFromType<10> // never
Enter fullscreen mode Exit fullscreen mode

To cover more cases for our StringFromType generic, we can chain more conditions exactly like nesting ternary operators in JavaScript.

type StringFromType<T> = T extends string
  ? 'string'
  : T extends boolean
  ? 'boolean'
  : T extends Error
  ? 'error'
  : never

type lorem = StringFromType<'lorem ipsum'> // 'string'
type isActive = StringFromType<false> // 'boolean'
type unassignable = StringFromType<TypeError> // 'error'
Enter fullscreen mode Exit fullscreen mode

Conditional types and unions

In the case of extending a union as a constraint, TypeScript will loop over each member of the union and return a union of its own:

type NullableString = string | null | undefined

type NonNullable<T> = T extends null | undefined ? never : T // Built-in type, FYI

type CondUnionType = NonNullable<NullableString> // evalutes to `string`
Enter fullscreen mode Exit fullscreen mode

TypeScript will test the constraint T extends null | undefined by looping over our union, string | null | undefined, one type at a time.

You can think of it as the following illustrative code:

type stringLoop = string extends null | undefined ? never : string // string

type nullLoop = null extends null | undefined ? never : null // never

type undefinedLoop = undefined extends null | undefined ? never : undefined // never

type ReturnUnion = stringLoop | nullLoop | undefinedLoop // string
Enter fullscreen mode Exit fullscreen mode

Because ReturnUnion is a union of string | never | never, it evaluates to string (see explanation above.)

You can see how abstracting the extended union into our generic allows us to create the built-in Extract and Exclude utility types in TypeScript:

type Extract<T, U> = T extends U ? T : never
type Exclude<T, U> = T extends U ? never : T
Enter fullscreen mode Exit fullscreen mode

Conditional types and functions

To check whether a type extends a certain function shape, the [Function](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md#:~:text=Avoid%20the%20Function,the%20new%20keyword) type must not be used. Instead, the following signature can be used to extend all possible functions:

type AllFunctions = (args: any[]) => any
Enter fullscreen mode Exit fullscreen mode

…args: any[] will cover zero and more arguments, while => any would cover any return type.

Using infer in TypeScript

The infer keyword compliments conditional types and cannot be used outside an extends clause. Infer allows us to define a variable within our constraint to be referenced or returned.

Take the built-in TypeScript ReturnType utility, for example. It takes a function type and gives you its return type:

type a = ReturnType<() => void> // void
type b = ReturnType<() => string | number> // string | number
type c = ReturnType<() => any> // any
Enter fullscreen mode Exit fullscreen mode

It does that by first checking whether your type argument (T) is a function, and in the process of checking, the return type is made into a variable, infer R, and returned if the check succeeds:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
Enter fullscreen mode Exit fullscreen mode

As previously mentioned, this is mainly useful for accessing and using types that are not available to us.

React prop types

In React, we often need to access prop types. To do that, React offers a utility type for accessing prop types powered by the infer keyword called ComponentProps.

type ComponentProps<
  T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>
> = T extends JSXElementConstructor<infer P>
  ? P
  : T extends keyof JSX.IntrinsicElements
  ? JSX.IntrinsicElements[T]
  : {}
Enter fullscreen mode Exit fullscreen mode

After checking that our type argument is a React component, it infers its props and returns them. If that fails, it checks that the type argument is an IntrinsicElements (div, button, etc.) and returns its props. If all fails, it returns {} which, in TypeScript, means “any non-null value”.

Infer keyword use cases

Using the infer keyword is often described as unwrapping a type. Here are some common uses of the infer keyword.

Function’s first argument:

This is the solution from our first example:

type GetFirstArgumentOfAnyFunction<T> = T extends (
  first: infer FirstArgument,
  ...args: any[]
) => any
  ? FirstArgument
  : never

type t = GetFirstArgumentOfAnyFunction<(name: string, age: number) => void> // string
Enter fullscreen mode Exit fullscreen mode

Function’s second argument:

type GetSecondArgumentOfAnyFunction<T> = T extends (
  first: any,
  second: infer SecondArgument,
  ...args: any[]
) => any
  ? SecondArgument
  : never

type t = GetSecondArgumentOfAnyFunction<(name: string, age: number) => void> // number
Enter fullscreen mode Exit fullscreen mode

Promise return type

type PromiseReturnType<T> = T extends Promise<infer Return> ? Return : T

type t = PromiseReturnType<Promise<string>> // string 
Enter fullscreen mode Exit fullscreen mode

Array type

type ArrayType<T> = T extends (infer Item)[] ? Item : T

type t = ArrayType<[string, number]> // string | number
Enter fullscreen mode Exit fullscreen mode

Conclusion

The infer keyword is a powerful tool that allows us to unwrap and store types while working with third-party TypeScript code. In this article, we explained various aspects of writing robust conditional types using the never keyword, extends keyword, unions, and function signatures.


Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript meetup header

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Top comments (0)