Simohamed Marhraoui Vue and React developer | Linux enthusiast | Interested in FOSS

Understanding infer in TypeScript

4 min read 1209

TypeScript Logo

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  ")}.`;
}

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] */

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 */ 

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

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'

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`

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

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

Conditional types and functions

To check whether a type extends a certain function shape, the Function type must not be used. Instead, the following signature can be used to extend all possible functions:

type AllFunctions = (…args: any[]) => any

…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

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;

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]
  : {}

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

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

Promise return type

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

type t = PromiseReturnType<Promise<string>> // string 

Array type

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

type t = ArrayType<[string, number]> // string | number

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.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Simohamed Marhraoui Vue and React developer | Linux enthusiast | Interested in FOSS

6 Replies to “Understanding infer in TypeScript”

  1. A literal [ ‘hello’ , ‘world’ ] in Typescript code is by default typed as a mutable array not a readonly tuple, but you can resolve this with `as const`.

    Although it was a two-arg string array when you created it, Typescript models it as a mutable array, because you could push(), pop() and so on. One way to defeat this type-widening, alex should be declared `as const` which prevents it from being considered mutable and makes push(), pop() a compiler error so it can never vary from being a two-value tuple.

    I really liked the learning associated with infer, (for when you can’t edit the function), but for the case where you can edit the function, I think a better fix is for the person type to be asserted readonly in the first place and to use `as const` when composing person objects, which allows the original code to compile…

    function describePerson(person: Readonly<{
    name: string;
    age: number;
    hobbies: Readonly; // tuple
    }>) {
    return `${person.name} is ${person.age} years old and love ${person.hobbies.join(” and “)}.`;
    }

    const alex = {
    name: ‘Alex’,
    age: 20,
    hobbies: [‘walking’, ‘cooking’] // type is [string, string]
    } as const;

    describePerson(alex)

    Getting this right means that you haven’t type-widened the alex object, to turn e.g. hobbies into [string,string] by declaring it as a Person. When you use `as const` the hobbies property can still be inferred by the editor as being the narrower [‘walking’,’cooking’]. This has saved me a million times where compiler and editor awareness of the values is needed to guard sensitive logic. For example, some other type might be {hobby:’cooking’|’walking’, favouriteOutdoorMeals:string[]} and the compiler can know that both values of alex.hobbies fulfil the hobby value. This is not possible after type-widening them to string.

    See also https://learntypescript.dev/10/l4-readonly-function-parameters and https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md

    You can see the above approach in the playground https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAEwKYGcICcYCNUAKqW6CAFAA7GlgBciASqgIbIIA2AngDwDeAsAChEiMMwC2qeuig4wAcwDcQkc3lTRIcfizLhiABZxcuGBnpNWHHgG0Zc+QBpE9mAoC6APkWIA9L8QoEAp2VCEAX08ASkQBfSxUIKwkAAMAEl4qEgQAOjFJcMQYdEQMrJoctVRCzhYSRDh2ZERmMGb2OAA3VFLM6lyjEzN0HIArODcyACIWtpEpqPCclL1woSEIBBkW0IAPRABeWJVRCQ0AcgBBPfPHE6r6ACYABjv9QdNzRBtzgHdmdgAazc8luiHOmzgwIU53cfgCUE4VCKJTsshBzlcHgiLRKmzAMj0QjQmBw+CI2TAZABqF2USAA

  2. Thank you so much for this great article. I didn’t get a sense of “infer” from official TS guide. But here it described perfectly

  3. This is so COOL! This article let me understand the concept of ‘infer’. Thanks a lot, Marhraoui 🙂

  4. For you who need to infer Function return Promise,

    type PromiseReturnType = T extends Promise ? Return : T
    type FunctionReturnType = T extends (…args: any[]) => infer R
    ? PromiseReturnType
    : any

Leave a Reply