• Note: There are currently no Chinese translations of the latest TypeScript official documentation available online, so there is a translation plan. Since I’m also a beginner to TypeScript, I can’t guarantee 100% accuracy in TypeScript translations, so I feel free to point out any errors in the comments section.
  • Translation content: The temporary translation content is TypeScript Handbook, and other parts of the translation document will be supplemented later.
  • Project address: typescript-doc-zh, if it helps you, you can click a star ~

The official document address of this chapter is Conditional Types

Conditions in the

At the heart of most applications, we need to decide which logic to execute based on the input. The same is true for JavaScript applications, but because values are introspective, which logic to execute depends on the type of input data. Condition types can then be used to describe the relationship between input types and output types.

interface Animal {
    live(): void;
}
interface Dog extends Animal {
    woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
		^
        // type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
	    ^
        // type Example2 = string
Copy the code

The form of a condition type is a bit like a conditional expression in JavaScript (condition? True branch: false branch) :

SomeType extends OtherType ? TrueType : FalseType;
Copy the code

When the type on the left side of extends can be assigned to the type on the right, you end up with the type in the first (true) branch, otherwise you get the type in the second (false) branch.

From the above example alone, the conditional type doesn’t seem very useful — even without it, we can figure out if Dog extends Animal is true and then choose the corresponding number or string type! But conditional types can be very powerful when used in conjunction with generics.

For example, let’s look at the following createLabel function:

interface IdLabel {
    id: number            /* Some attributes */
}
interface NameLabel {
    name: string          /* Other attributes */  
}
function createLabel(id: number) :IdLabel;
function createLabel(name: string) :NameLabel;
function createLabel(nameOrId: string | number) :IdLabel | NameLabel;
function createLabel(nameOrId: string | number) :IdLabel | NameLabel {
    throw "unimplemented";
}
Copy the code

The createLabel function uses overloading to select different output types based on different input types. Please note that there are some problems with this:

  1. If a library has to make the same choices over and over again throughout the API, this becomes cumbersome.
  2. We need to create three overloads: the first two are for specific input types (stringnumber), the last is for the most general case (the input type isstring | number ). Once thecreateLabelBy increasing the number of new types that can be handled, the number of overloads increases exponentially.

Instead, we can code the logic of the above code into a condition type:

type NameOrId<T extends number | string> = T extends nummber
? IdLabel
: NameLabel;
Copy the code

Next, we can use this condition type to reduce the overloaded function to an unoverloaded function:

function createLabel<T extends number | string> (idOrName: T) :NameOrId<T> {
    throw "unimplemented";
}
let a = createLabel("typescript");
    ^
   // let a: NameLabel
 
let b = createLabel(2.8);
    ^
  // let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
    ^
 // let c: NameLabel | IdLabel
Copy the code

Conditional type constraint

Often, checks in condition types give us some new information. Just as type contraction with type protection leads to a more specific type, true branches of conditional types further constrain generics by the types we examine.

Take the following code for example:

type MessageOf<T> = T['message'];
//Type '"message"' cannot be used to index type 'T'.
Copy the code

In this code, TypeScript throws an error because it cannot determine whether T has a message attribute. We can constrain T so TypeScript doesn’t report errors:

type MessageOf<T extends { message: unknown }> = T['message'];

interface Email {
    message: string;
}

type EmailMessageContents = MessageOf<Emial>;
			^
          // type EmailMessageContents = string      
Copy the code

However, what if we wanted MessageOf to accept any type and default to never when the message attribute does not exist? We can move the constraint out and introduce the condition type:

type MessageOf<T> = T extends { message: unknown } ? T['message'] : never

interface Email {
    message: string;
}

interface Dog {
    bark(): void;
}

type EmialMessageContents = MessageOf<Email>;
			^
          // type EmialMessageContents = string
type DogMessageContents = MessageOf<Dog>;
             ^
          // type DogMessageContents = never                
Copy the code

In the true branch of conditional types, TypeScript knows that T will have a message attribute.

Let’s do another example. We can write a Flatten function that flattens the array type to the type of the elements in the array and keeps the original type for non-array types:

type Flatten<T> = T extends any[]? T[number] : T;

// Extract the element type
type Str = Flatten<string[] >; ^// type Str = string

// Keep the same type
type Num = Flatten<number>;
	  ^
    // type Num = number      
Copy the code

When Flatten accepts array types, it uses number as an index to extract element types from array types string[]. If it accepts a non-array type, it returns the given original type.

Make inferences in condition types

In the example above, we use the condition type to apply the constraint and extract the type. Because this operation is common, the condition type provides an easier way to do it.

Condition types provide the infer keyword, which allows us to infer a type of condition and apply it to the true branch. For example, in the Flatten function above, we can infer the type of an array element directly, rather than “manually” extracting the element through index access:

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Copy the code

Instead of specifying in the true branch how to extract the element types of the T-array, we use the Infer keyword to declaratively introduce a new generic type variable Item. This frees us from thinking about how to find the structure of the type we are interested in.

We can use the infer keyword to write some useful aliases for tool types. For example, in some simple cases, we can extract the type of the return value from the function type:

type GetReturnType<Type> = Type extends(... args:never[]) => infer Rerturn
? Return
: never;

type Num = GetReturnType<() = > number>;
	  ^
   // type Num = number      
type Str = GetReturnType<(x: string) = > string>;
	  ^
   // type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) = > boolean[] >; ^// type Bools = boolean[]            
Copy the code

If you infer from a type that has multiple calling signatures (such as the type of an overloaded function), then you infer only for the last signature (which is the most common case).

declare function stringOrNum(x: string) :number;
declare function stringOrNum(x: number) :string;
declare function stringOrNum(x: string | number) :string | number;
 
type T1 = ReturnType<typeof stringOrNum>;
     ^
   // type T1 = string | number
Copy the code

The type of condition that can be assigned

When a condition type is applied to a generic, the condition type is assignable if a union type is given. As an example, look at the following code:

type ToArray<Type> = Type extends any ? Type[] : never;
Copy the code

If we pass a union type to toArray, the condition type will be applied to each member of the union type.

type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
		   ^
         // type StrArrOrNumArr = string[] | number[]      
Copy the code

StrArrOrNumArr allocations occur in the following union types:

string | number
Copy the code

Each member of the union type is then effectively mapped to the following array:

ToArray<string> | ToArray<number>;
Copy the code

The resulting array looks like this:

string[] | number[];
Copy the code

Normally, this is the behavior we expect. If you want to circumvent this behavior, you can wrap the left and right sides of the extends keyword in square brackets.

type ToArrayNonDist<Type> = [Type] extends [any]? Type[] :never;

// 'StrArrOrNumArr' is no longer a union type
type StrArrOrNumArr = ToArrayNonDist<string | number>;
		  ^
        // type StrArrOrNumArr = (string | number)[]   
Copy the code