Conditional types are not just for switching behavior based on comparison — they can be used with an infer keyword to access sub-parts of type information within a larger type
Type inference in conditional types
In the same release where conditional types were added to TypeScript a new infer keyword was added as well. This keyword, which can only be used in the context of a condition expression (within a conditional type declaration) is an important tool for being able to extract out pieces of type information from other types.
A motivating use case
Let’s consider a practical example: You use a library that provides a well-typed function, but does not expose independent types for the arguments the function takes.
Let’s imagine that there’s a fruit-market npm package, which only exports a createOrder function.
tsTry// @filename: fruit-market.ts////////////////////////////////////////////////////////////////////////////////////// NOT EXPORTED //////////////////////////////////////////////////////////////////////////////////////typeAppleVarieties = 'fuji' | 'gala' | 'honeycrisp' | 'granny smith';typeOrangeVarieties = 'navel' | 'valencia' | 'blood orange' | 'cara cara';typeAllergies = 'peach' | 'kiwi' | 'strawberry' | 'pineapple';typeRipeness = 'green' | 'ripe' | 'overripe';typeQuantityRange = {min : number;max : number;};typeFruitOrderItem <Varieties extends string> = {variety :Varieties ;pricePerUnit : number;quantity : number;totalPrice : number;};typeFruitOrder = {apples :FruitOrderItem <AppleVarieties >[];oranges :FruitOrderItem <OrangeVarieties >[];subtotal : number;salesTax : number;grandTotal : number;};typeFruitOrderPreferences = {apples : {preferredVarieties :AppleVarieties [];avoidSeeds : boolean;organicOnly : boolean;ripeness :Ripeness ;quantity :QuantityRange ;};oranges : {preferredVarieties :OrangeVarieties [];seedlessOnly : boolean;ripeness :Ripeness ;quantity :QuantityRange ;};allergies :Allergies [];prefersLocalProduce : boolean;};//////////////////////////////////////////////////////////////////////////////////////// EXPORTED ////////////////////////////////////////////////////////////////////////////////////////export functioncreateOrder (prefs :FruitOrderPreferences ):FruitOrder {console .log (prefs )return {apples : [],oranges : [],subtotal : 0.00,salesTax : 0.00,grandTotal : 0.00,}}
Look at all that great type information — it’s a shame that none of it is exported!
Our goal is to create a well-typed variable to hold a value of type FruitOrderPreferences, so we can assemble the right data together, log it, and then pass it to the createOrder to create that FruitOrder. A starting point for this code is below. All we need to do is replace GetFirstArg<T> = any with a more meaningful type expression.
tsTry// @filename: index.tsimport {createOrder } from './fruit-market';typeGetFirstArg <T > = any;constprefs :GetFirstArg <typeofcreateOrder > = {}createOrder (prefs )
The infer keyword
The infer keyword gives us an important tool to solve this problem — it lets us extract and obtain type information from larger types, by capturing pieces of types into a newly-declared type params.
Here’s an example of it in action:
tsTry/*** If the type `P` passed in is some kind of `PromiseLike<T>`* (where `T` is a new type param), extract `T` and return it.* If `P` is not some subtype of `PromiseLike<any>`, pass the* type `P` straight through and return it*/typeUnwrapPromise <P > =P extendsPromiseLike <inferT > ?T :P ;typetest1 =UnwrapPromise <Promise <string>>typetest2 =UnwrapPromise <Promise <[string[], number[]]>>typetest3 =UnwrapPromise <number>
Here’s a breakdown of what the conditional type means
tsTrytypeUnwrapPromise <P > =P extendsPromiseLike <inferT > ?T :P ;// ---------------------
If P is a subset of PromiseLike<any>
tsTrytypeUnwrapPromise <P > =P extendsPromiseLike <inferT > ?T :P ;// ---------
Extract the typeParam of PromiseLike<?> and store it in a new typeParam T
tsTrytypeUnwrapPromise <P > =P extendsPromiseLike <inferT > ?T :P ;// ---
And then return type T
tsTrytypeUnwrapPromise <P > =P extendsPromiseLike <inferT > ?T :P ;// ---
Otherwise return the original typeParam P
Let’s go back to our need to define GetFirstArg<T> from our fruit-market library.
First, let’s make sure the condition in the conditional type works the way we want it to, allowing us to return one type if the typeParam looks like a function with at least one argument, and another (never) otherwise. We’ll begin with the type for functions that have at least one argument, and make the type of that argument generic since we know we’ll want to extract it in a future step.
tsTrytypeOneArgFn <A = any> = (firstArg :A , ..._args : any[]) => void
I’m using the variable name _args starting with an underscore (_) here to indicate that I don’t care about any arguments after the first one, but I’m happy to tolerate their presence and ignore them.
Now let’s use a conditional type and a test function to make sure we’re returning never in the right case, and something other than never (I’m using string[] temporarily) when we have function with at least one argument. Remember that the never is advisable here because it effectively erases incompatible aspects of the type, in the case of a union type, just as we saw in Extract<T> and Exclude<T>.
tsTrytypeOneArgFn <A = any> = (firstArg :A , ..._args : any[]) => voidtypeGetFirstArg <T extendsOneArgFn >=T extendsOneArgFn ? string[]: never;// Test casefunctionfoo (x : string,y : number) {return null}// Should be string[]typet1 =GetFirstArg <typeoffoo >
Next, let’s bring in the infer keyword, and the type param it creates on the fly
tsTrytypeOneArgFn <A = any> = (firstArg :A , ..._args : any[]) => voidtypeGetFirstArg <T >=T extendsOneArgFn <inferR >?R : never;// Test casefunctionfoo (x : string,y : number) {return null}typet1 =GetFirstArg <typeoffoo >
There we go! string is what we were looking for! Let’s bring it back to our fruit market example
tsTry// @filename: node_modules/fruit-market.tstypeAppleVarieties = 'fuji' | 'gala' | 'honeycrisp' | 'granny smith';typeOrangeVarieties = 'navel' | 'valencia' | 'blood orange' | 'cara cara';typeAllergies = 'peach' | 'kiwi' | 'strawberry' | 'pineapple';typeRipeness = 'green' | 'ripe' | 'overripe';typeQuantityRange = {min : number;max : number;};Type '{}' is missing the following properties from type 'FruitOrderPreferences': apples, oranges, allergies, prefersLocalProduce2739Type '{}' is missing the following properties from type 'FruitOrderPreferences': apples, oranges, allergies, prefersLocalProducetypeFruitOrderItem <Varieties extends string> = {variety :Varieties ;pricePerUnit : number;quantity : number;totalPrice : number;};typeFruitOrder = {apples :FruitOrderItem <AppleVarieties >[];oranges :FruitOrderItem <OrangeVarieties >[];subtotal : number;salesTax : number;grandTotal : number;};typeFruitOrderPreferences = {apples : {preferredVarieties :AppleVarieties [];avoidSeeds : boolean;organicOnly : boolean;ripeness :Ripeness ;quantity :QuantityRange ;};oranges : {preferredVarieties :OrangeVarieties [];seedlessOnly : boolean;ripeness :Ripeness ;quantity :QuantityRange ;};allergies :Allergies [];prefersLocalProduce : boolean;};export functioncreateOrder (prefs :FruitOrderPreferences ):FruitOrder {console .log (prefs )return {apples : [],oranges : [],subtotal : 0.00,salesTax : 0.00,grandTotal : 0.00,}}// @filename: index.tsimport {createOrder } from 'fruit-market';typeOneArgFn <A extends {}> = (firstArg :A , ..._ : any[]) => voidtypeGetFirstArg <T >=T extendsOneArgFn <inferR >?R : never;constprefs :GetFirstArg <typeofcreateOrder > = {}createOrder (prefs )
Awesome! We’re getting an error that indicates we have the desired type!
Constraints on infer
TypeScript 5 allows type param constraints to be expressed on inferred type params. For example, what if we wanted to extract the first element of a tuple, but only if it’s a subtype of string
Without any kind of constraint, we’re just getting the first element of the tuple, no matter what it is
tsTrytypeGetFirstStringIshElement <T > =T extends readonly [inferS ,..._ :any[]] ?S : neverconstt1 = ["success", 2, 1, 4] asconst constt2 = [4, 54, 5] asconst letfirstT1 :GetFirstStringIshElement <typeoft1 >letfirstT2 :GetFirstStringIshElement <typeoft2 >
if we add a constraint
diff+ infer S extends string,- infer S,
we get the desired result, with firstT2 evaluating to never
tsTrytypeGetFirstStringIshElement <T > =T extends readonly [inferS extends string,..._ :any[]] ?S : neverconstt1 = ["success", 2, 1, 4] asconst constt2 = [4, 54, 5] asconst letfirstT1 :GetFirstStringIshElement <typeoft1 >letfirstT2 :GetFirstStringIshElement <typeoft2 >
Ok, we’re only extracting the type of the first element of the tuple in the event that the first element (S) is some subtype of string, but it’s not great that we see that never. Really this should be an error, and we can make it an error via the use of a type param constraint.
diff- type GetFirstStringIshElement<T>+ type GetFirstStringIshElement<T extends readonly [string, ...any[]]>
And now we’ll get a compile error
tsTrytypeGetFirstStringIshElement <T extends readonly [string, ...any[]]> =T extends readonly [inferS extends string,..._ :any[]] ?S : neverconstt1 = ["success", 2, 1, 4] asconst constt2 = [4, 54, 5] asconst letfirstT1 :GetFirstStringIshElement <typeoft1 >letType 'readonly [4, 54, 5]' does not satisfy the constraint 'readonly [string, ...any[]]'. Type at position 0 in source is not compatible with type at position 0 in target. Type 'number' is not assignable to type 'string'.2344Type 'readonly [4, 54, 5]' does not satisfy the constraint 'readonly [string, ...any[]]'. Type at position 0 in source is not compatible with type at position 0 in target. Type 'number' is not assignable to type 'string'.firstT2 :GetFirstStringIshElement <typeoft2 >
This may feel a little redundant, but it’s important to realize that the condition on the conditional type, and the constraint on the type param serve two different purposes.
- The type param constraint describes what is allowed for 
T. Anything that doesn’t align with the constraint will cause a compiler error - The condition in the conditional type is sort of an equivalent to control flow for types. It will never generate a compile error, because it’s essentially just an 
if/else 
Utility types that use infer
TypeScript includes a number of utility types, which are kind of like a type-based standard library. A couple of these are essentially just based around generics, conditional types and the infer keyword. Let’s take a close look at them
Parameters<T>
This is very similar to the GetFirstArg<T> type we created, but more generalized
ts/*** Obtain the parameters of a function type in a tuple*/type Parameters/*** The typeParam passed in, must be some subtype of a call signature,* which can take any number of arguments of any types, and can* have any return type*/<T extends (...args: any) => any>/*** As long as `T` matches a call signature, capture all of the args* (as a ...rest) parameter in a new tuple typeParam `P`*/= T extends (...args: infer P) => any? P // and then return the tuple: never; // or return never, if the condition is not matched
ConstructorParameters<T>
This is very similar to the Parameters<T> but for construct signatures instead of call signatures
ts/*** Obtain the parameters of a constructor function type in a tuple*/type ConstructorParameters/*** The typeParam passed in, must be some subtype of a construct* signature.** The `abstract` keyword lets this also work with abstract classes,* which can potentially have an `abstract` constructor*/<T extends abstract new (...args: any) => any>/*** As long as `T` matches a construct signature, capture all of the* args (as a ...rest) parameter in a new tuple typeParam `P`*/= T extends abstract new (...args: infer P) => any? P // and then return the tuple: never; // or return never, if the condition is not matched
ReturnType<T>
This utility type captures the return type of a call signature
ts/*** Obtain the return type of a function type*/type ReturnType/*** The typeParam passed in must be some subtype of a call signature*/<T extends (...args: any) => any>/*** As long as `T` matches the call signature, capture the return type* in a new typeParam `R`*/= T extends (...args: any) => infer R? R // and then return it: any; // otherwise return any
InstanceType<T>
Very similar to ReturnType<T>, this utility type takes a type with a construct signature, and extracts the type it instantiates. As is the case with ConstructorParameters<T>, we’re essentially just inserting a few abstract new keywords
ts/*** Obtain the return type of a constructor function type*/type InstanceType/*** The typeParam passed in must be some subtype of a construct signature*/<T extends abstract new (...args: any) => any>/*** As long as `T` matches the construct signature, capture the return* type in a new typeParam `R`*/= T extends abstract new (...args: any) => infer R? R // and then return it: any; // otherwise return any
ThisParameterType<T> and OmitThisParameter<T>
As long as you know what a this type is, ThisParameterType<T> follows the last few examples so closely, that it probably doesn’t need much explanation.
ts/*** Extracts the type of the 'this' parameter of a function type, or 'unknown'* if the function type has no 'this' parameter.*/type ThisParameterType<T>= T extends (this: infer U, ...args: never) => any? U: unknown;
OmitThisParameter<T> is another story. It involves multiple conditional types and multiple infers. Let’s break it down so that we can understand how it works
ts/*** Removes the 'this' parameter from a function type.*/type OmitThisParameter<T>/*** If `ThisParameterType<T>` evaluates to `unknown`, it means one of two* things:* (1) `T` is not a call signature type* (2) `T` is a call signature type, with a `this` type of `undefined`** In either of these cases, we effectively short circuit, and return* the `unknown`*/= unknown extends ThisParameterType<T>? T/*** In this branch, we know that `T` is a call signature, with a* non-undefined `this` type** Here we are inferring _both_ the tuple type representing the* arguments, _and_ the return type into two new typeParams, `A` and* `R`, respectively*/: T extends (...args: infer A) => infer R/*** Here, we are effectively reconstructing the function* _without_* the `this` type, using both of our `infer`red typeParams, `A`* and `R`*/? (...args: A) => R/*** essentially this is an unreachable branch. It doesn't really* matter what this type is*/: T;