The infer keyword
Use infer in a conditional type to pull one type out of another: a list's item, a function's return and parameters, a promise's value, with constraints.
- 01Read an infer patternExplain how
T extends (infer Item)[] ? Item : neverlines up a pattern with a type and names the part under the blank. - 02Pull types out of functions and promisesWrite helpers that find a function's return type, its parameters, and the value a promise gives, including a promise inside a promise.
- 03Add a constraint to inferUse
infer First extends stringso the found type can go where only text is allowed, and say what happens when it doesn't fit.
Pull the item type out of a cart
Your code often has a big type and needs one part of it. A library gives you the type of a whole cart, a list of items, and you want the type of one item. A function returns an object, and you want that object's type. TypeScript can pull the part out for you, so you never write the same type twice. The keyword that does it is infer:
type ItemOf<List> = List extends (infer Item)[] ? Item : never; type Cart = { name: string; price: number }[];type CartItem = ItemOf<Cart>;// ^?const tea: CartItem = { name: "Tea", price: 3 };console.log(tea.name);Line 1 is a conditional type, from the Conditional types lesson: A extends B ? X : Y means "if type A fits B, the result is X, otherwise Y". The new part is infer Item. Read line 1 as a question with a blank in it:
- Is
Lista list of something,(…)[]? - If yes, call that something
Item, and the result isItem. - If no, the result is
never, the type that has no values (any, unknown & never covers it).
infer Item is the blank. It tells TypeScript: "work out what goes here, and call it Item".
Line 3 is the cart type: a list ([]) of objects with a name and a price. Line 4 asks ItemOf for the type of one item, and the query on line 5 shows the answer: { name: string; price: number }. You never wrote that type a second time. Line 6 uses it for the tea, and line 7 prints Tea.
Cart. Both end in [], so they match, and Item names the part under the blank. Line 4 gives that part a name of its own: CartItem.A parcel arrives with a printed label: "Contents: ____". Someone wrote "mugs" in the blank. You don't open the box. You know the printed words, so you read whatever was written in the blank. A parcel without that label tells you nothing, so you do something else with it.
- In real life: The printed label: "Contents: ____"
- In TypeScript: The pattern
(infer Item)[] - In real life: The blank on the label
- In TypeScript:
infer Item - In real life: Reading "mugs" from the blank
- In TypeScript:
Itembecomes the item type - In real life: A parcel with no such label
- In TypeScript: A type that doesn't fit the pattern: the false branch
infer puts a named blank in the pattern of a conditional type. TypeScript lines the pattern up with your type. If it fits, the blank's name holds the type that sits there, and you can use it in the true branch. If it doesn't fit, you get the false branch.
How infer names part of a type
Why does the blank need a keyword? Try writing the pattern without it. TypeScript reports two errors, on purpose here:
type ItemOf<List> = List extends Item[] ? Item : never;Cannot find name 'Item' means TypeScript has never heard of a type called Item. It reports it twice: once in the pattern Item[], and once in the true branch ? Item. Every type name has to be made somewhere, for example with type Item = …, as a type parameter in <…>, or with infer. Here is what each part of the working version does:
List extends …asks: does the type inListfit the pattern?(infer Item)[]is the pattern: a list of something.infer Itemmakes a new type name,Item, for whatever stands in that spot. The round brackets keepinfer Itemtogether; leave them out and TypeScript can't read the line.? Itemis the true branch: if the pattern fits, the result is the type TypeScript found.: neveris the false branch: the result when the pattern doesn't fit.
Working out the type in the blank is called inference, the same word as when TypeScript works out that let total = 3 is a number (Type annotations & inference). That's where the keyword's name comes from. Now try it: type a different list type on line 3, and watch the answer after ^? on line 5 and the errors under the code.
type ItemOf<List> = List extends (infer Item)[] ? Item : never; type Tags = string[];type Tag = ItemOf<Tags>;// ^?const firstTag: Tag = "sale";console.log(firstTag);string[]stringNo errorsstring[] is a list of strings, so the pattern matches and Item is string. Line 6 stores the text "sale", which is a string, so there are no errors.
With number[], Tag becomes number, so storing "sale" on line 6 is an error. With (string | number)[] it's fine again. A plain string isn't a list at all: the pattern doesn't fit, ItemOf gives never, and nothing can be stored in a never. The same happens with readonly string[], a list you can't change, because the pattern describes a list you can change. Arrays and tuples covers read-only lists.
The name infer makes only lives in the true branch. Using it in the false branch is an error, on purpose here:
type ItemOf<List> = List extends (infer Item)[] ? Item : Item;The error is Cannot find name 'Item' again, this time once, for the last Item. When the pattern doesn't fit, there is nothing in the blank, so there is no Item to give back.
Return and parameter types of a function
A pattern can be any type with a blank in it, including a function type. A function type looks like an arrow function with types instead of code: (price: number) => string takes a number and returns a string (Function types covers them). Put the blank where the return type goes:
type ReturnOf<Fn> = Fn extends (...args: any[]) => infer Result ? Result : never; function getUser() { return { name: "Asha", cartItems: 2 };} type User = ReturnOf<typeof getUser>;// ^?const user: User = { name: "Ravi", cartItems: 0 };console.log(user.name);Line 1's pattern is (...args: any[]) => infer Result. The part before the arrow, ...args: any[], is a rest parameter that accepts any arguments at all, so every function fits. It has to be any[] here, not unknown[]: a function that only takes a string can't take every value, so it wouldn't fit (...args: unknown[]). The blank sits after the arrow, where the return type goes.
Lines 3 to 5 are an ordinary function that returns a user. On line 7, typeof getUser gives the function's type. In a type, typeof followed by a value's name means "the type of this value" (keyof & typeof covers it); here that is () => { name: string; cartItems: number }. The query on line 8 shows what ReturnOf pulls out: { name: string; cartItems: number }. Line 9 makes another user of that type, and line 10 prints Ravi. When getUser changes, User changes with it.
Move the blank before the arrow, and you get the parameters instead:
type ParamsOf<Fn> = Fn extends (...args: infer Params) => any ? Params : never; function searchProducts(query: string, page: number) { return `${query} on page ${page}`;} type SearchArgs = ParamsOf<typeof searchProducts>;// ^?const lastSearch: SearchArgs = ["tea", 2];console.log(searchProducts(...lastSearch));Line 1 puts the blank where ...args goes. A rest parameter collects every argument into one list, so Params is all of the parameters together, as a tuple: a list with a fixed length and a type for each place. The query on line 8 shows [query: string, page: number]. The words query: and page: are labels for each place; they don't change the type. Line 9 saves a search as a tuple, and line 10 spreads it back into a call and prints tea on page 2.
searchProducts has two parts: its parameters and what it returns. ParamsOf puts its blank over the parameters and gets a tuple. ReturnOf puts its blank over the return type and gets string.A pattern can also pick out one parameter. Here is the type of the event a click handler takes:
type FirstParam<Fn> = Fn extends (first: infer First, ...rest: any[]) => any ? First : never; function onCartClick(event: MouseEvent, productId: number) { console.log(productId);} type ClickEvent = FirstParam<typeof onCartClick>;// ^?Line 1's pattern has two parameters: first: infer First catches the first one, and ...rest: any[] accepts any others. Lines 3 to 5 are a click handler that takes the browser's MouseEvent and a product id. The query on line 8 shows ClickEvent is MouseEvent.
TypeScript comes with helpers that work this way: ReturnType<Fn>, Parameters<Fn>, and Awaited<T> for promises. They are written with infer too. In your projects, use them. The lesson ReturnType, Awaited & more covers them; writing your own here shows how they work.
Unwrapping promises and lists
An async function always returns a promise, even when its code returns a plain object. So its return type is wrapped in Promise<…>. To reach the value inside, put the blank inside the promise:
type ReturnOf<Fn> = Fn extends (...args: any[]) => infer Result ? Result : never;type Unwrap<T> = T extends Promise<infer Value> ? Value : T; async function fetchUser() { return { name: "Asha", cartItems: 2 };} type Reply = ReturnOf<typeof fetchUser>;// ^?type User = Unwrap<Reply>;// ^?Line 1 is ReturnOf from before. Line 2 is new: its pattern is Promise<infer Value>, a promise of something. Lines 4 to 6 are an async function that returns a user. Line 8 asks for its return type, and the query on line 9 shows it's a promise: Promise<{ name: string; cartItems: number }>. Line 10 unwraps it, and the query on line 11 shows the user object itself.
Look at Unwrap's false branch: it's T, not never. When T isn't a promise, you get it back unchanged. You choose what the false branch gives. Try it with different replies:
type Unwrap<T> = T extends Promise<infer Value> ? Value : T; type Reply = Promise<string>;type Name = Unwrap<Reply>;// ^?const name: Name = "Asha";console.log(name);stringPromise<string>No errorsprints AshaPromise<string> fits Promise<infer Value>, so Value is string and Name is string. Line 6 stores "Asha", so there are no errors.
A promise of text gives string. Plain text gives string too, from the false branch. A promise of a number gives number, so line 6 is an error. A promise in a promise gives Promise<string>: Unwrap opened one layer and stopped. When the code runs, the types are gone, so it prints Asha every time.
To open every layer, let the true branch call Unwrap again on what it found:
type UnwrapAll<T> = T extends Promise<infer Value> ? UnwrapAll<Value> : T; type Reply = Promise<Promise<string>>;type Name = UnwrapAll<Reply>;// ^?const name: Name = "Asha";console.log(name);Line 1's true branch is UnwrapAll<Value>: it unwraps what it found, again and again, until the false branch gives back something that isn't a promise. A type that uses itself like this is called recursive. Line 3 is a promise in a promise, and the query on line 5 shows string. Line 6 stores "Asha" without an error, and line 7 prints it.
Promise<Promise<string>> is a promise inside a promise. Unwrap opens the outer one and stops, so you get Promise<string>. UnwrapAll calls itself on what it finds, until no promise is left: string.Lists work the same way. ItemOf unwraps a list. For a tuple, a pattern can name one place, like the first or the last. A breadcrumb trail at the top of a shop page is a good example:
type FirstOf<List> = List extends [infer First, ...unknown[]] ? First : never;type LastOf<List> = List extends [...unknown[], infer Last] ? Last : never; type Trail = ["Home", "Shop", "Tea"];type Start = FirstOf<Trail>;// ^?type Here = LastOf<Trail>;// ^?type Nothing = FirstOf<[]>;// ^?In a tuple pattern, ...unknown[] means "any number of other items, of any type". unknown is the type every value fits. Line 1 puts the blank first and the rest after it. Line 2 puts the rest first and the blank last. Line 4 is the trail. The query on line 6 shows "Home", the first page, and the query on line 8 shows "Tea", the last. Line 9 tries an empty tuple, which has no first item, so the query on line 10 shows never.
The table below collects the patterns so far. Read each row as: this pattern, the types it matches, and what its blank holds.
| The pattern | What it matches | What the blank holds |
|---|---|---|
(infer Item)[] | A list, like string[] or CartItem[]. | The type of one item: string, CartItem. |
Promise<infer Value> | A promise, like the reply of an async function. | The value the promise gives when it's done. |
(...args: any[]) => infer Result | Any function. | What the function returns. |
(...args: infer Params) => any | Any function. | Its parameters, as a tuple like [query: string, page: number]. |
[infer First, ...unknown[]] | A tuple with at least one item. | The type of the first item. |
infer with a constraint
Sometimes the type you find has to go somewhere picky. Here, PageTitle only takes text, and you want to give it the first page of a trail. TypeScript reports an error, on purpose here:
type PageTitle<Name extends string> = { title: Name; path: string }; type FirstPage<Trail> = Trail extends [infer First, ...unknown[]] ? PageTitle<First> : never;Line 1 says PageTitle's Name must be a string: Name extends string is a constraint, as in Generic constraints. Lines 3 to 5 find the first item of a trail and pass it to PageTitle. Line 4 gets Type 'First' does not satisfy the constraint 'string'. It means: PageTitle only takes text, and nothing promises that First is text. A trail's first item could be any type.
The fix is a constraint on the blank itself: infer First extends string.
type PageTitle<Name extends string> = { title: Name; path: string }; type FirstPage<Trail> = Trail extends [infer First extends string, ...unknown[]] ? PageTitle<First> : never; type Home = FirstPage<["Home", "Shop"]>;// ^?type Missing = FirstPage<[404, "Shop"]>;// ^?const home: Home = { title: "Home", path: "/" };console.log(home.title);The constraint on line 3 does two jobs:
- In the true branch,
Firstis known to be a string, so line 4 may pass it toPageTitle. - When the first item isn't text, the pattern doesn't fit, and you get the false branch.
The query on line 8 shows Home is { title: "Home"; path: string }. Line 9 tries a trail that starts with the number 404, and the query on line 10 shows never. Line 11 makes a home page of the Home type, and line 12 prints Home.
"Home" is text, so it passes and First is "Home". 404 is a number, so the pattern doesn't match and FirstPage gives never.The constraint can be any type, like a union. Here it accepts an id that is a string or a number:
type IdOf<T> = T extends { id: infer Id extends string | number } ? Id : never; type ProductId = IdOf<{ id: number; name: string }>;// ^?type OrderId = IdOf<{ id: "A-7"; total: number }>;// ^?type FlagId = IdOf<{ id: boolean }>;// ^?Line 1's pattern is an object with an id. Objects with more properties fit too, as long as they have an id. Line 3's product has a number id, and the query on line 4 shows number. Line 5's order has the id "A-7", and the query on line 6 shows exactly "A-7", a literal type. Line 7's id is a boolean, which the constraint turns away, so the query on line 8 shows never.
Types from an API helper
Here is how infer helps in a real app. A fetchCart function stands in for a call to the server, and its reply is the only place the cart's shape is written. The rest of the page works its types out from it. Press the buttons to change what fetchCart returns on line 6:
type ItemOf<List> = List extends (infer Item)[] ? Item : never;type Unwrap<T> = T extends Promise<infer Value> ? Value : T;type ReturnOf<Fn> = Fn extends (...args: any[]) => infer Result ? Result : never; async function fetchCart() { return { items: [{ name: "Tea", price: 3 }], total: 3 };} type CartReply = Unwrap<ReturnOf<typeof fetchCart>>;type CartLine = ItemOf<CartReply["items"]>;// ^? function showLine(line: CartLine) { return `${line.name}: $${line.price}`;}console.log(showLine({ name: "Cake", price: 4 }));Cake: $4{ name: string; price: number }No errorsCartLine is { name: string; price: number }, worked out from line 6. Line 16 passes a cake with the number 4, which fits, so there are no errors and it prints Cake: $4.
Lines 1 to 3 are the three helpers from this lesson. Lines 5 to 7 are fetchCart. Read line 9 from the inside out: typeof fetchCart is the function's type, ReturnOf gives its promise, and Unwrap opens the promise. On line 10, CartReply["items"] is the type of the reply's items property (the Indexed access types lesson covers this), and ItemOf gives one item. The query on line 11 shows CartLine. Lines 13 to 15 show one line of the cart, and line 16 prints Cake: $4.
With Price as text, CartLine's price becomes a string, and line 16, which passes a number, gets an error. With Add a size, CartLine gets a size, and line 16's cake has none. You changed one line, and TypeScript pointed at the one place that no longer matches. Without infer, you would have written the item type by hand, and it would have quietly gone out of date.
Now sort some types. Each card uses one of these helpers. Every helper gives never when its pattern doesn't match:
type ItemOf<List> = List extends (infer Item)[] ? Item : never;type ReturnOf<Fn> = Fn extends (...args: any[]) => infer Result ? Result : never;type FirstOf<List> = List extends [infer First, ...unknown[]] ? First : never;type FirstText<List> = List extends [infer First extends string, ...unknown[]] ? First : never;ItemOf<string[]>ItemOf<"tea">ReturnOf<() => number>ReturnOf<{ total: 7 }>FirstOf<["Home", "Shop"]>FirstOf<[]>FirstText<[3, "tea"]>ItemOf<[string, number]>
Sort each type by whether its helper's pattern matches, or the helper takes its false branch and gives never.
Common mix-ups
infer only works inside the pattern of a conditional type. Here it is on its own, an error on purpose:
type Cart = { name: string; price: number }[];type CartItem = infer Item;Line 2 gets 'infer' declarations are only permitted in the 'extends' clause of a conditional type. It means infer needs a pattern to line up with a type, and only the part after extends is a pattern. Line 2 has no type to compare with, so there is nothing to work out.
A function with overloads, several call signatures for one function, has a surprise too:
type ReturnOf<Fn> = Fn extends (...args: any[]) => infer Result ? Result : never; function formatPrice(price: number): string;function formatPrice(price: string): number;function formatPrice(price: number | string) { return typeof price === "number" ? String(price) : Number(price);} type Formatted = ReturnOf<typeof formatPrice>;// ^?Line 3 says a number gives back a string, and line 4 says a string gives back a number. Line 5 is the code that runs. The query on line 10 shows number: when a function has several signatures, infer reads the last one, line 4. It can't pick the signature that fits a call, because there is no call here.
- "I can write infer anywhere in a type." No. Only after
extendsin a conditional type. Anywhere else is error 1338. - "infer looks at my values while the page runs." No. It works on types, before the code runs. The playgrounds print
AshaandCake: $4whatever the types say. - "When the pattern doesn't match, infer gives unknown." No. You get the false branch, exactly as you wrote it:
neverinItemOf,Titself inUnwrap. - "
(infer Item)[]matches every kind of list." No. Areadonly string[]doesn't fit it. Writereadonly (infer Item)[]to match both kinds. - "infer picks the overload that fits my call." No. It reads the last signature.
- "I have to write ReturnOf myself in real code." No. TypeScript has
ReturnType,Parameters, andAwaitedbuilt in. Writing your own shows how they work.
infer isn't the only way to reach part of a type. The table below compares it with reading a property type by name, CartReply["items"] from the API helper. Read each row as a question, then each way's answer.
| Question | By name: CartReply["items"] | With infer |
|---|---|---|
| How do you say which part you want? | By name: the property called items. | By its place in a pattern, like a list's item or what a function returns. |
| Can it reach a function's return type? | No. A function type has no property that holds it. | Yes: Fn extends (...args: any[]) => infer Result. |
| What if the part isn't there? | An error: the property doesn't exist on that type. | No error. The false branch is used, often never. |
| Where can you write it? | In any type. | Only after extends in a conditional type. |
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code: the errors update as you type, and a // ^? line under a type name shows what TypeScript found. Try a guess first, then use the hints.
What type is Price? Guess first. Then point at Price, or add a // ^? line under it.
type ItemOf<List> = List extends (infer Item)[] ? Item : never; type Prices = number[];type Price = ItemOf<Prices>;type ItemOf<List> = List extends (infer Item)[] ? Item : never; type Prices = number[];type Price = ItemOf<Prices>;// ^?number[] is a list of numbers, so the pattern matches and Item is number. The query on line 5 shows type Price = number.
What type is Theme? Read Unwrap's false branch before you answer.
type Unwrap<T> = T extends Promise<infer Value> ? Value : T; type Theme = Unwrap<string>;type Unwrap<T> = T extends Promise<infer Value> ? Value : T; type Theme = Unwrap<string>;// ^?string isn't a promise, so the pattern doesn't match. Unwrap's false branch is T, so you get string back unchanged.
Write LastOf so it gives the last page of a breadcrumb trail. What type is Here then?
// Replace never with a pattern that finds the last item.type LastOf<List> = never; type Here = LastOf<["Home", "Shop", "Tea"]>;type LastOf<List> = List extends [...unknown[], infer Last] ? Last : never; type Here = LastOf<["Home", "Shop", "Tea"]>;// ^?[...unknown[], infer Last] matches any tuple with at least one item and names the last one. For the trail, Last is "Tea".
FirstParam gives unknown, so line 8 can't read the key property. Change line 1 so it finds the first parameter. What type does it find for onSearch?
type FirstParam<Fn> = Fn extends (...args: any[]) => any ? unknown : never; function onSearch(event: KeyboardEvent, query: string) { console.log(query);} type SearchEvent = FirstParam<typeof onSearch>;const pressed: SearchEvent["key"] = "Enter";type FirstParam<Fn> = Fn extends (first: infer First, ...rest: any[]) => any ? First : never; function onSearch(event: KeyboardEvent, query: string) { console.log(query);} type SearchEvent = FirstParam<typeof onSearch>;// ^?const pressed: SearchEvent["key"] = "Enter";console.log(pressed);The blank sits on the first parameter, so First is KeyboardEvent. A KeyboardEvent has a key property, so line 9's SearchEvent["key"] is a string and the error is gone. It prints Enter.
Line 3 has an error. Fix it with a constraint on infer. Then, what type is value in FontSize?
type Setting<Value extends string | number> = { value: Value; label: string }; type FirstSetting<List> = List extends [infer First, ...unknown[]] ? Setting<First> : never; type FontSize = FirstSetting<[16, "large"]>;const fontSize: FontSize = { value: 16, label: "Font size" };type Setting<Value extends string | number> = { value: Value; label: string }; type FirstSetting<List> = List extends [infer First extends string | number, ...unknown[]] ? Setting<First> : never; type FontSize = FirstSetting<[16, "large"]>;// ^?const fontSize: FontSize = { value: 16, label: "Font size" };console.log(fontSize.value);With infer First extends string | number, First is known to fit Setting, so the error is gone. The first item is 16, so value is exactly 16, and the code prints 16.
Line 5 has an error. What type does this Unwrap give for Reply? Then change Unwrap so it opens every layer.
type Unwrap<T> = T extends Promise<infer Value> ? Value : T; type Reply = Promise<Promise<string>>;type Name = Unwrap<Reply>;const name: Name = "Asha";console.log(name);type Unwrap<T> = T extends Promise<infer Value> ? Unwrap<Value> : T; type Reply = Promise<Promise<string>>;type Name = Unwrap<Reply>;// ^?const name: Name = "Asha";console.log(name);The starter's Unwrap gives Promise<string>: it opens the outer promise only, so line 5 gets an error. Calling Unwrap<Value> in the true branch keeps opening until no promise is left, so Name is string and the error is gone.
Check your understanding
For each question, line the pattern up with the type. Does it fit? If yes, what sits in the blank? If no, what does the false branch say? The quiz code isn't checked on the page, so no error list or type hint gives the answer away.
Question 1 of 7In
List extends (infer Item)[] ? Item : never, what doesinfer Itemdo?Choose an answer to see the explanation.
Question 2 of 7What type is
Flag?Read the code, then predicttype ItemOf<List> = List extends (infer Item)[] ? Item : never; type Flag = ItemOf<boolean[]>;Choose an answer to see the explanation.
Question 3 of 7
stringisn't a list. What type isSize?Read the code, then predicttype ItemOf<List> = List extends (infer Item)[] ? Item : never; type Size = ItemOf<string>;Choose an answer to see the explanation.
Question 4 of 7What type is
SaveArgs?Read the code, then predicttype ParamsOf<Fn> = Fn extends (...args: infer Params) => any ? Params : never; type SaveArgs = ParamsOf<(id: number, note: string) => void>;Choose an answer to see the explanation.
Question 5 of 7This
Unwrapdoesn't call itself. What type isTotal?Read the code, then predicttype Unwrap<T> = T extends Promise<infer Value> ? Value : T; type Total = Unwrap<Promise<Promise<number>>>;Choose an answer to see the explanation.
Question 6 of 7Where can you write
infer?Choose an answer to see the explanation.
Question 7 of 7With the constraint
extends string, what type isLabel?Read the code, then predicttype FirstText<List> = List extends [infer First extends string, ...unknown[]] ? First : never; type Label = FirstText<[3, "tea"]>;Choose an answer to see the explanation.
Key takeaways
infer Nameputs a named blank in the pattern of a conditional type, afterextends.- If the pattern fits, the name holds the type in the blank, and you use it in the true branch.
- If it doesn't fit, you get the false branch, exactly as you wrote it: often
never, or the type itself. - Put the blank where you need it: a list's item, a function's return type or parameters, a promise's value, a tuple's first or last place.
- A true branch that calls its own type again, like
UnwrapAll, opens every layer. infer First extends stringadds a constraint: the blank only accepts that type, so you can use it where that type is needed.
Remember the one-liner.infer is a named blank in a pattern: TypeScript lines the pattern up with your type and fills the blank in.
Coming next: Mapped types. They go through every property of a type and change each one: make them all optional, read-only, or a new type.