At the heart of most useful programs, we have to make decisions based on input. JavaScript programs are no different, but given the fact that values can be easily introspected, those decisions are also based on the types of the inputs. Conditional types help describe the relation between the types of inputs and outputs.
Try
interfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;type Example1 = numbertypeExample2 =RegExp extendsAnimal ? number : string;type Example2 = string
Conditional types take a form that looks a little like conditional expressions (condition ? trueExpression : falseExpression
) in JavaScript:
Try
SomeType extendsOtherType ?TrueType :FalseType ;
When the type on the left of the extends
is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).
From the examples above, conditional types might not immediately seem useful - we can tell ourselves whether or not Dog extends Animal
and pick number
or string
! But the power of conditional types comes from using them with generics.
For example, let’s take the following createLabel
function:
Try
interfaceIdLabel {id : number /* some fields */;}interfaceNameLabel {name : string /* other fields */;}functioncreateLabel (id : number):IdLabel ;functioncreateLabel (name : string):NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel {throw "unimplemented";}
These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:
- If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.
- We have to create three overloads: one for each case when we’re sure of the type (one for
string
and one fornumber
), and one for the most general case (taking astring | number
). For every new typecreateLabel
can handle, the number of overloads grows exponentially.
Instead, we can encode that logic in a conditional type:
Try
typeNameOrId <T extends number | string> =T extends number?IdLabel :NameLabel ;
We can then use that conditional type to simplify our overloads down to a single function with no overloads.
Try
functioncreateLabel <T extends number | string>(idOrName :T ):NameOrId <T > {throw "unimplemented";}leta =createLabel ("typescript");let a: NameLabelletb =createLabel (2.8);let b: IdLabelletc =createLabel (Math .random () ? "hello" : 42);let c: NameLabel | IdLabel
Conditional Type Constraints
Often, the checks in a conditional type will provide us with some new information. Just like with narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constrain generics by the type we check against.
For example, let’s take the following:
Try
typeType '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.MessageOf <T > =T ["message"];
In this example, TypeScript errors because T
isn’t known to have a property called message
. We could constrain T
, and TypeScript would no longer complain:
Try
typeMessageOf <T extends {message : unknown }> =T ["message"];interfacemessage : string;}typeEmailMessageContents =MessageOf <type EmailMessageContents = string
However, what if we wanted MessageOf
to take any type, and default to something like never
if a message
property isn’t available? We can do this by moving the constraint out and introducing a conditional type:
Try
typeMessageOf <T > =T extends {message : unknown } ?T ["message"] : never;interfacemessage : string;}interfaceDog {bark (): void;}typeEmailMessageContents =MessageOf <type EmailMessageContents = stringtypeDogMessageContents =MessageOf <Dog >;type DogMessageContents = never
Within the true branch, TypeScript knows that T
will have a message
property.
As another example, we could also write a type called Flatten
that flattens array types to their element types, but leaves them alone otherwise:
Try
typeFlatten <T > =T extends any[] ?T [number] :T ;// Extracts out the element type.typeStr =Flatten <string[]>;type Str = string// Leaves the type alone.typeNum =Flatten <number>;type Num = number
When Flatten
is given an array type, it uses an indexed access with number
to fetch out string[]
’s element type. Otherwise, it just returns the type it was given.
Inferring Within Conditional Types
We just found ourselves using conditional types to apply constraints and then extract out types. This ends up being such a common operation that conditional types make it easier.
Conditional types provide us with a way to infer from types we compare against in the true branch using the infer
keyword. For example, we could have inferred the element type in Flatten
instead of fetching it out “manually” with an indexed access type:
Try
typeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
Here, we used the infer
keyword to declaratively introduce a new generic type variable named Item
instead of specifying how to retrieve the element type of T
within the true branch. This frees us from having to think about how to dig through and probing apart the structure of the types we’re interested in.
We can write some useful helper type aliases using the infer
keyword. For example, for simple cases, we can extract the return type out from function types:
Try
typeGetReturnType <Type > =Type extends (...args : never[]) => inferReturn ?Return : never;typeNum =GetReturnType <() => number>;type Num = numbertypeStr =GetReturnType <(x : string) => string>;type Str = stringtypeBools =GetReturnType <(a : boolean,b : boolean) => boolean[]>;type Bools = boolean[]
When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types.
Try
declare functionstringOrNum (x : string): number;declare functionstringOrNum (x : number): string;declare functionstringOrNum (x : string | number): string | number;typeT1 =ReturnType <typeofstringOrNum >;type T1 = string | number
Distributive Conditional Types
When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:
Try
typeToArray <Type > =Type extends any ?Type [] : never;
If we plug a union type into ToArray
, then the conditional type will be applied to each member of that union.
Try
typeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;type StrArrOrNumArr = string[] | number[]
What happens here is that StrArrOrNumArr
distributes on:
Try
string | number;
and maps over each member type of the union, to what is effectively:
Try
ToArray <string> |ToArray <number>;
which leaves us with:
Try
string[] | number[];
Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends
keyword with square brackets.
Try
typeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'StrArrOrNumArr' is no longer a union.typeStrArrOrNumArr =ToArrayNonDist <string | number>;type StrArrOrNumArr = (string | number)[]