preface

The official TypeScript documentation has been updated for a long time, but the Chinese documentation I could find was still in older versions. Therefore, some new and revised chapters are translated and sorted out.

This article is compiled from the chapter on “Conditional Types” in the TypeScript Handbook.

This paper does not strictly follow the translation of the original text, and some of the content has also been explained and supplemented.

Conditional Types

Most of the time, we need to determine the value of the output based on the value of the input, and we also need to determine the type of the output value based on the type of the input value. Conditional types are 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

Condition types are written somewhat like conditional expressions in JavaScript (condition? TrueExpression: falseExpression) :

SomeType extends OtherType ? TrueType : FalseType;
Copy the code

It may not be obvious from this example, but it can be useful when used with generics. Let’s take the createLabel function above as an example:

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
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

Function overloading is used here to describe how createLabel makes different decisions and returns different types based on the type of the input value. Look out for a few things:

  1. If a library has to make the same choices after iterating through the API over and over again, it can become very cumbersome.
  2. We had to create three overloads, one to handle explicitly known types, and we wrote an overload for each type (here is one)string, one isnumber), and one for the general case (receive onestring | number). If you add a new type, the number of overloads increases exponentially.

We could have written the logic in the condition type:

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

Using this condition type, we can simplify function overloading:

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 Constraints

Often, using condition types gives us some new information. Just as Type Guards play a role of narrowing types to provide us with a more specific type, the true branch of conditional types may further constrain generics. For example:

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

TypeScript reports errors because T doesn’t know there is a property called message. We can constrain T so that TypeScript will no longer report errors:

type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
Copy the code

However, what if we wanted MessgeOf to be passed in any type, but to return the default type such as never when the value passed in has no message attribute?

We can move the constraint out and use a condition type:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;           
// type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;          
// type DogMessageContents = never
Copy the code

In the True branch, TypeScript knows that T has a message property.

For another example, we write a Flatten type that gets the type of an array element, and returns the type passed if it is not an array:

type Flatten<T> = T extends any[]? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[] >;// type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;  
// type Num = number
Copy the code

Note that the number index in the index access type is used to get the type of the array element.

Inferring Within Conditional Types

Conditional types provide the infer keyword, which can infer the type from the type being compared and then refer to that inference in the True branch. Infer: With infer, we modify the implementation of Flatten instead of “manually” retrieving the index access type:

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

Here we use the infer keyword to declare a new type variable Item, rather than explicitly saying how to get the element type of T in the True branch. This frees us from thinking about how to extract the type structure we want from the type structure we are interested in.

We can also use the infer keyword to write useful helper type aliases. For example, we can get the type returned by a function:

type GetReturnType<Type> = Type extends(... args:never[]) => infer Return
  ? 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

When inferring a type from a multi-call signature (such as an overloaded function), the inference is based on the last signature, because this signature is typically the signature that handles all cases.

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

Distributive Conditional Types

When a conditional type is used in a generic type, if a union type is passed in, it becomes distributive, for example:

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

If we pass a union type in ToArray, the condition type is 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

Let’s analyze what happens in StrArrOrNumArr. Here’s the type we pass in:

string | number;
Copy the code

Next, iterate over the members of the union type, equivalent to:

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

So the end result is:

string[] | number[];
Copy the code

This is usually the expected behavior, and if you want to avoid it, you can wrap each part of the extends keyword in square brackets.

type ToArrayNonDist<Type> = [Type] extends [any]? Type[] :never;
 
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]
Copy the code

The TypeScript series

  1. The Narrowing of TypeScript
  2. TypeScript More on Functions
  3. The TypeScript Object Type
  4. The TypeScript Generics
  5. TypeScript 之 Keyof Type Operator
  6. TypeScript Typeof Type Operator
  7. TypeScript Indexed Access Types

Wechat: “MQyqingfeng”, add me Into Hu Yu’s only readership group.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.