infer
in TypeScriptEditor’s note: This article was last updated by Ikeh Akinyemi on 21 October 2024 to cover using infer
in generic functions, the role infer
plays with tuple types, arrays, and unions, plus some code corrections.
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 loves ${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
.
It would be easy to think that the best approach to resolve the error is to hardcode-type the value of the hobbies
field using as [string, string]
, but what happens if you have to use the alex
object elsewhere in your code that expects string[]
?
A more involved but efficient approach to easily have the proper type checking and auto-completion on the alex
object would be to use TypeScript conditional typing. We’ll cover more of it later in this article, but let’s see it in action for now:
type GetFirstArgumentOfAnyFunction<T> = T extends (first: infer FirstArgument, ...args: any[]) => any ? FirstArgument : never; const alex: GetFirstArgumentOfAnyFunction<typeof describePerson> = { name: "Alex", age: 20, hobbies: ["walking", "cooking"], }; describePerson(alex); /* No TypeScript errors */
The conditional typing and infer
keyword in TypeScript allow us to take a type and isolate any piece of it for later use.
never
typeIn TypeScript, never
is treated as the “no-value” type. You will often see it being used as a dead-end type and commonly used with conditional typing. A simple example of using never
is in a union type such as string | never
; in TypeScript, this will be evaluated 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 (or an empty set, i.e., ∅ set). The union of such two sets is 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. On the other hand, any
serves as a wildcard type that encompasses all possible types. In the following sections, we’ll see how both never
and any
are instrumental in conditional types.
Conditional types modify a type based on whether or not it satisfies a certain constraint. It works similarly to ternaries in JavaScript.
extends
keywordIn 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-compatible.
Thus, we can have a generic that checks whether a constraint is met and returns 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'
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 the 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
infer
in TypeScriptThe infer
keyword compliments conditional types and cannot be used outside an extends
clause. infer
is used within conditional types to declare a type variable within our constraint to capture types dynamically within the extends
clause of a conditional type.
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.
Infer
keyword use casesUsing the infer
keyword is often described as unwrapping a type. The following are some common use cases for the infer
keyword.
Here’s a simple example:
type ExtractStringType<T> = T extends `${infer U}` ? U : never;
In this sample, if T
is a string, the infer U
statement will capture the actual string type within U
. This allows us to work with the extracted string type.
The infer
keyword is particularly useful when working with union types:
type ExtractNumberType<T> = T extends `${infer U}` ? `${U}` extends `${number}` ? U : never : never;
In the above snippet, ExtractNumberType
takes a union type T
and extracts any numeric types from it. The infer U
statement captures the individual types within the union, and the nested conditional type checks if each extracted type is a number type.
You can also use the infer
keyword to extract types from arrays:
type ExtractArrayElementType<T extends readonly any[]> = T extends readonly (infer U)[] ? U : never;
In the above snippet, ExtractArrayElementType
takes an array type T
and extracts the element type of the array. The infer U
statement captures the element type, and the conditional type checks if T
is an array of U
.
You can use infer
in more complex recursive type operations, such as flattening nested arrays:
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T; type NestedArray = number\[][\][]; type Flat = Flatten<NestedArray>; // This will be 'number'
Here, we used Flatten<T>
to recursively check if T
is an array. Then, we used infer U
to determine the type of the nested array’s elements. This continues to unwrap each level of nested arrays until it reaches a non-array type.
We can also extract and manipulate types from tuple types, including inferring the return type from a tuple parameter:
type TupleToUnion<T extends any[]> = T extends [infer U, ...infer Rest] ? U | TupleToUnion<Rest> : never; function getTupleHead<T extends any[]>(tuple: [...T]): TupleToUnion<T> { return tuple[0]; } const result1 = getTupleHead(["hello", 42]); // result1 is inferred as string | number const result2 = getTupleHead([true, false, true]); // result2 is inferred as boolean
In the above sample, we defined a type TupleToUnion
that takes a tuple type T
and converts it into a union. We used the infer
keyword to extract the first element from the tuple, infer U
, and the remaining ones with infer Rest
. Then, the union type was formed by applying TupleUnion
recursively over what was left of the tuple.
The function getTupleHead
receives a tuple as a parameter and returns its first element. We used […T]
to create a tuple over T
, and then the return type is inferred using the TupleToUnion
, the result being a conversion of the tuple to a union type.
In React, we often need to access prop types. React offers the utility type, ComponentProps
, for accessing prop types powered by the infer
keyword:
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 belongs to IntrinsicElements
(div
, button
, etc.) and returns its props. If all fails, it returns {}
which, in TypeScript, means “any non-null value.”
infer
in generic functionsYou can define types for your function’s parameters and return types based on the inferred types from the function arguments or return values.
Let’s start by inferring the parameter types:
function makeArray<T extends unknown[]>(...args: T): T { return args; } const numberArray = makeArray(1, 2, 3); // numberArray is inferred as number[] const stringArray = makeArray('a', 'b', 'c'); // stringArray is inferred as string[]
The infer
keyword is not explicitly used here, but TypeScript infers the type of T
based on the arguments passed to the function.
Next, let’s infer the return types:
function getFirst<T extends unknown[]>(arr: T): T extends [infer U, ...unknown[]] ? U : never { return arr[0] as T extends [infer U, ...unknown[]] ? U : never; } const firstNumber = getFirst([1, 2, 3]); // firstNumber is inferred as number const firstString = getFirst(['a', 'b', 'c']); // firstString is inferred as string
Here, the generic type T
is constrained to be an array type (T extends unknown[]
).
The conditional type T
is an array with at least one element. If so, it infers the type of the first element using infer U
. The return type is then set to U
if the condition is met, or never
otherwise. The type assertion in the return statement helps TypeScript correctly infer the return type. This allows the function to work with arrays of any type, correctly inferring the type of the first element.
Next, let’s see how we can use a functional interface together with infer
to extract and use type information from the function parameters and return types. This way, we can define more generic and reusable types that can adapt to different input and output types:
interface Mapper<T, U> { (input: T): U; } type MapArray<T, U> = { mapArray: <V extends Array<T>>(arr: V, fn: Mapper<T, U>) => Array<U>; }; const mapObjectsImplementation: MapArray<{ id: string }, string> = { mapArray: (arr, fn) => arr.map(obj => fn(obj)), }; const result = mapObjectsImplementation.mapArray( [{ id: "1" }, { id: "2" }], obj => obj.id ); // result is inferred as string[]
Here, we have a functional interface Mapper<T, U>
, which represents a function that will take an input of type T
and return a value of type U
. We also defined the type MapArray<T, U>
, which has a mapArray
property, a function that will take an array of type V
(where V
is constrained to be an array of T
), and a Mapper<T, U>
function. Then, the mapArray
function takes the provided Mapper
function, applies it to each element in the array, and returns a new array of type U
.
In our implementation of mapObjectsImplementation
, we specify the types { id: string }
and string
for T
and U
, respectively. This way, TypeScript can correctly infer the types for the mapArray
function and its arguments.
Keep in mind that the infer
keyword has its restrictions when inferring generic type parameters reliant on other type parameters. TypeScript’s inference method primarily depends on the types of function arguments and their return values to infer types, without restricting the generic type parameters during the inference process. That is why there are times when TypeScript cannot correctly infer dependent types automatically.
The infer
keyword in TypeScript is useful for manipulating or extracting types because it lets you write expressive code that is safe when it comes to types. This is especially useful when dealing with generic types, complex type operations, or third-party code because it can capture types and save them on the fly.
Throughout the article, we covered various use cases of infer
, which include basic type extractions and advanced scenarios involving unions, arrays, tuples, and functional interfaces. We also looked at the complexities of conditional types using the extends
keyword and the never
type to create powerful type utilities.
The infer
keyword is a vital component of any TypeScript developer’s toolkit, and hopefully, you have learned how to use it to craft expressive third-party libraries.
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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
8 Replies to "Understanding <code>infer</code> in TypeScript"
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
Thank you. Here I understood the infer
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
This is so COOL! This article let me understand the concept of ‘infer’. Thanks a lot, Marhraoui 🙂
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
Good stuff
“`
function getFirst(arr: T): T extends [infer U, …unknown[]] ? U : never {
return arr[0];
}
const firstNumber = getFirst([1, 2, 3]); // firstNumber is inferred as number
const firstString = getFirst([‘a’, ‘b’, ‘c’]); // firstString is inferred as string
“`
This example is not working in TS Playground. Both variables get `never`.
Hey Serg! Thanks for letting us know. We contacted the writer, and he updated the code snippet and its explanation.