Conditional types take a form that looks a little like conditional expressions () in JavaScript:

    1. ts
      SomeType extends OtherType ? 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:

    1. ts
      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";
      }

    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.

    Instead, we can encode that logic in a conditional type:

    1. ts
      type NameOrId<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.

    1. ts
      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

    For example, let’s take the following:

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

    In this example, TypeScript errors because isn’t known to have a property called message. We could constrain T, and TypeScript would no longer complain:

    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:

    1. ts
      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

    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:

    1. ts
      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

    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.

    1. ts
      type Flatten<Type> = Type extends Array<infer Item> ? 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:

      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.

      1. ts
        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

      Distributive Conditional Types

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

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

      1. ts
        type ToArray<Type> = Type extends any ? Type[] : never;
         
        type StrArrOrNumArr = ToArray<string | number>;
        type StrArrOrNumArr = string[] | number[]

      What happens here is that StrArrOrNumArr distributes on:

      1. ts
        string | number;

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

      1. ts
        ToArray<string> | ToArray<number>;
      1. ts
        string[] | number[];

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

      1. ts