Was this page helpful?

Conditional Types

These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:

  1. If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.
  2. We have to create three overloads: one for each case when we’resure of the type (one forstring 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:

ts
typeNameOrId<Textendsnumber |string> =Textendsnumber
?IdLabel
:NameLabel;
Try

We can then use that conditional type to simplify our overloads down to a single function with no overloads.

ts
functioncreateLabel<Textendsnumber |string>(idOrName:T):NameOrId<T> {
throw"unimplemented";
}
 
leta =createLabel("typescript");
let a: NameLabel
 
letb =createLabel(2.8);
let b: IdLabel
 
letc =createLabel(Math.random() ?"hello" :42);
let c: NameLabel | IdLabel
Try

Conditional Type Constraints

Often, the checks in a conditional type will provide us with some new information.Just like 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:

ts
typeMessageOf<T> =T["message"];
Type '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.
Try

In this example, TypeScript errors becauseT isn’t known to have a property calledmessage.We could constrainT, and TypeScript would no longer complain:

ts
typeMessageOf<Textends {message:unknown }> =T["message"];
 
interfaceEmail {
message:string;
}
 
typeEmailMessageContents =MessageOf<Email>;
type EmailMessageContents = string
Try

However, what if we wantedMessageOf to take any type, and default to something likenever if amessage property isn’t available?We can do this by moving the constraint out and introducing a conditional type:

ts
typeMessageOf<T> =Textends {message:unknown } ?T["message"] :never;
 
interfaceEmail {
message:string;
}
 
interfaceDog {
bark():void;
}
 
typeEmailMessageContents =MessageOf<Email>;
type EmailMessageContents = string
 
typeDogMessageContents =MessageOf<Dog>;
type DogMessageContents = never
Try

Within the true branch, TypeScript knows thatTwill have amessage property.

As another example, we could also write a type calledFlatten that flattens array types to their element types, but leaves them alone otherwise:

ts
typeFlatten<T> =Textendsany[] ?T[number] :T;
 
// Extracts out the element type.
typeStr =Flatten<string[]>;
type Str = string
 
// Leaves the type alone.
typeNum =Flatten<number>;
type Num = number
Try

WhenFlatten is given an array type, it uses an indexed access withnumber to fetch outstring[]’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 theinfer keyword.For example, we could have inferred the element type inFlatten instead of fetching it out “manually” with an indexed access type:

ts
typeFlatten<Type> =TypeextendsArray<inferItem> ?Item :Type;
Try

Here, we used theinfer keyword to declaratively introduce a new generic type variable namedItem instead of specifying how to retrieve the element type ofType 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 theinfer keyword.For example, for simple cases, we can extract the return type out from function types:

ts
typeGetReturnType<Type> =Typeextends (...args:never[])=>inferReturn
?Return
:never;
 
typeNum =GetReturnType<()=>number>;
type Num = number
 
typeStr =GetReturnType<(x:string)=>string>;
type Str = string
 
typeBools =GetReturnType<(a:boolean,b:boolean)=>boolean[]>;
type Bools = boolean[]
Try

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from thelast 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.

ts
declarefunctionstringOrNum(x:string):number;
declarefunctionstringOrNum(x:number):string;
declarefunctionstringOrNum(x:string |number):string |number;
 
typeT1 =ReturnType<typeofstringOrNum>;
type T1 = string | number
Try

Distributive Conditional Types

When conditional types act on a generic type, they becomedistributive when given a union type.For example, take the following:

ts
typeToArray<Type> =Typeextendsany ?Type[] :never;
Try

If we plug a union type intoToArray, then the conditional type will be applied to each member of that union.

ts
typeToArray<Type> =Typeextendsany ?Type[] :never;
 
typeStrArrOrNumArr =ToArray<string |number>;
type StrArrOrNumArr = string[] | number[]
Try

What happens here is thatToArray distributes on:

ts
string |number;
Try

and maps over each member type of the union, to what is effectively:

ts
ToArray<string> |ToArray<number>;
Try

which leaves us with:

ts
string[] |number[];
Try

Typically, distributivity is the desired behavior.To avoid that behavior, you can surround each side of theextends keyword with square brackets.

ts
typeToArrayNonDist<Type> = [Type]extends [any] ?Type[] :never;
 
// 'ArrOfStrOrNum' is no longer a union.
typeArrOfStrOrNum =ToArrayNonDist<string |number>;
type ArrOfStrOrNum = (string | number)[]
Try