Conditional Types

TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings.A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

  1. T extends U ? X : Y

The type above means when T is assignable to U the type is X, otherwise the type is Y.

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables.Whether to resolve or defer is determined as follows:

  • First, given types T' and U' that are instantiations of T and U where all occurrences of type parameters are replaced with any, if T' is not assignable to U', the conditional type is resolved to Y. Intuitively, if the most permissive instantiation of T is not assignable to the most permissive instantiation of U, we know that no instantiation will be and we can just resolve to Y.
  • Next, for each type variable introduced by an infer (more later) declaration within U collect a set of candidate types by inferring from T to U (using the same inference algorithm as type inference for generic functions). For a given infer type variable V, if any candidates were inferred from co-variant positions, the type inferred for V is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred for V is an intersection of those candidates. Otherwise, the type inferred for V is never.
  • Then, given a type T'' that is an instantiation of T where all infer type variables are replaced with the types inferred in the previous step, if T'' is definitely assignable to U, the conditional type is resolved to X. The definitely assignable relation is the same as the regular assignable relation, except that type variable constraints are not considered. Intuitively, when a type is definitely assignable to another type, we know that it will be assignable for all instantiations of those types.
  • Otherwise, the condition depends on one or more type variables and the conditional type is deferred.

Example

  1. type TypeName<T> =
  2. T extends string ? "string" :
  3. T extends number ? "number" :
  4. T extends boolean ? "boolean" :
  5. T extends undefined ? "undefined" :
  6. T extends Function ? "function" :
  7. "object";
  8. type T0 = TypeName<string>; // "string"
  9. type T1 = TypeName<"a">; // "string"
  10. type T2 = TypeName<true>; // "boolean"
  11. type T3 = TypeName<() => void>; // "function"
  12. type T4 = TypeName<string[]>; // "object"

Distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types.Distributive conditional types are automatically distributed over union types during instantiation.For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

Example

  1. type T10 = TypeName<string | (() => void)>; // "string" | "function"
  2. type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
  3. type T11 = TypeName<string[] | number[]>; // "object"

In instantiations of a distributive conditional type T extends U ? X : Y, references to T within the conditional type are resolved to individual constituents of the union type (i.e. T refers to the individual constituents after the conditional type is distributed over the union type).Furthermore, references to T within X have an additional type parameter constraint U (i.e. T is considered assignable to U within X).

Example

  1. type BoxedValue<T> = { value: T };
  2. type BoxedArray<T> = { array: T[] };
  3. type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
  4. type T20 = Boxed<string>; // BoxedValue<string>;
  5. type T21 = Boxed<number[]>; // BoxedArray<number>;
  6. type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;

Notice that T has the additional constraint any[] within the true branch of Boxed<T> and it is therefore possible to refer to the element type of the array as T[number]. Also, notice how the conditional type is distributed over the union type in the last example.

The distributive property of conditional types can conveniently be used to filter union types:

  1. type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
  2. type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U
  3. type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
  4. type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
  5. type T32 = Diff<string | number | (() => void), Function>; // string | number
  6. type T33 = Filter<string | number | (() => void), Function>; // () => void
  7. type NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from T
  8. type T34 = NonNullable<string | number | undefined>; // string | number
  9. type T35 = NonNullable<string | string[] | null | undefined>; // string | string[]
  10. function f1<T>(x: T, y: NonNullable<T>) {
  11. x = y; // Ok
  12. y = x; // Error
  13. }
  14. function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
  15. x = y; // Ok
  16. y = x; // Error
  17. let s1: string = x; // Error
  18. let s2: string = y; // Ok
  19. }

Conditional types are particularly useful when combined with mapped types:

  1. type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
  2. type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
  3. type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
  4. type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
  5. interface Part {
  6. id: number;
  7. name: string;
  8. subparts: Part[];
  9. updatePart(newName: string): void;
  10. }
  11. type T40 = FunctionPropertyNames<Part>; // "updatePart"
  12. type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
  13. type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void }
  14. type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }

Similar to union and intersection types, conditional types are not permitted to reference themselves recursively. For example the following is an error.

Example

  1. type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // Error

Type inference in conditional types

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred.Such inferred type variables may be referenced in the true branch of the conditional type.It is possible to have multiple infer locations for the same type variable.

For example, the following extracts the return type of a function type:

  1. type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Conditional types can be nested to form a sequence of pattern matches that are evaluated in order:

  1. type Unpacked<T> =
  2. T extends (infer U)[] ? U :
  3. T extends (...args: any[]) => infer U ? U :
  4. T extends Promise<infer U> ? U :
  5. T;
  6. type T0 = Unpacked<string>; // string
  7. type T1 = Unpacked<string[]>; // string
  8. type T2 = Unpacked<() => string>; // string
  9. type T3 = Unpacked<Promise<string>>; // string
  10. type T4 = Unpacked<Promise<string>[]>; // Promise<string>
  11. type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string

The following example demonstrates how multiple candidates for the same type variable in co-variant positions causes a union type to be inferred:

  1. type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
  2. type T10 = Foo<{ a: string, b: string }>; // string
  3. type T11 = Foo<{ a: string, b: number }>; // string | number

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

  1. type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
  2. type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
  3. type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number

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. declare function foo(x: string): number;
  2. declare function foo(x: number): string;
  3. declare function foo(x: string | number): string | number;
  4. type T30 = ReturnType<typeof foo>; // string | number

It is not possible to use infer declarations in constraint clauses for regular type parameters:

  1. type ReturnType<T extends (...args: any[]) => infer R> = R; // Error, not supported

However, much the same effect can be obtained by erasing the type variables in the constraint and instead specifying a conditional type:

  1. type AnyFunction = (...args: any[]) => any;
  2. type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

Predefined conditional types

TypeScript 2.8 adds several predefined conditional types to lib.d.ts:

  • Exclude<T, U> – Exclude from T those types that are assignable to U.
  • Extract<T, U> – Extract from T those types that are assignable to U.
  • NonNullable<T> – Exclude null and undefined from T.
  • ReturnType<T> – Obtain the return type of a function type.
  • InstanceType<T> – Obtain the instance type of a constructor function type.

Example

  1. type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
  2. type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
  3. type T02 = Exclude<string | number | (() => void), Function>; // string | number
  4. type T03 = Extract<string | number | (() => void), Function>; // () => void
  5. type T04 = NonNullable<string | number | undefined>; // string | number
  6. type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
  7. function f1(s: string) {
  8. return { a: 1, b: s };
  9. }
  10. class C {
  11. x = 0;
  12. y = 0;
  13. }
  14. type T10 = ReturnType<() => string>; // string
  15. type T11 = ReturnType<(s: string) => void>; // void
  16. type T12 = ReturnType<(<T>() => T)>; // {}
  17. type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
  18. type T14 = ReturnType<typeof f1>; // { a: number, b: string }
  19. type T15 = ReturnType<any>; // any
  20. type T16 = ReturnType<never>; // any
  21. type T17 = ReturnType<string>; // Error
  22. type T18 = ReturnType<Function>; // Error
  23. type T20 = InstanceType<typeof C>; // C
  24. type T21 = InstanceType<any>; // any
  25. type T22 = InstanceType<never>; // any
  26. type T23 = InstanceType<string>; // Error
  27. type T24 = InstanceType<Function>; // Error

Note: The Exclude type is a proper implementation of the Diff type suggested here. We’ve used the name Exclude to avoid breaking existing code that defines a Diff, plus we feel that name better conveys the semantics of the type. We did not include the Omit<T, K> type because it is trivially written as Pick<T, Exclude<keyof T, K>>.