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:
tsT 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'
andU'
that are instantiations ofT
andU
where all occurrences of type parameters are replaced withany
, ifT'
is not assignable toU'
, the conditional type is resolved toY
. Intuitively, if the most permissive instantiation ofT
is not assignable to the most permissive instantiation ofU
, we know that no instantiation will be and we can just resolve toY
. - Next, for each type variable introduced by an
infer
(more later) declaration withinU
collect a set of candidate types by inferring fromT
toU
(using the same inference algorithm as type inference for generic functions). For a giveninfer
type variableV
, if any candidates were inferred from co-variant positions, the type inferred forV
is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred forV
is an intersection of those candidates. Otherwise, the type inferred forV
isnever
. - Then, given a type
T''
that is an instantiation ofT
where allinfer
type variables are replaced with the types inferred in the previous step, ifT''
is definitely assignable toU
, the conditional type is resolved toX
. 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
tstype TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T0 = TypeName<string>; // "string" type T1 = TypeName<"a">; // "string" type T2 = TypeName<true>; // "boolean" type T3 = TypeName<() => void>; // "function" 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
tstype T10 = TypeName<string | (() => void)>; // "string" | "function" type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined" 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
tstype BoxedValue<T> = { value: T }; type BoxedArray<T> = { array: T[] }; type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>; type T20 = Boxed<string>; // BoxedValue<string>; type T21 = Boxed<number[]>; // BoxedArray<number>; 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:
tstype Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T32 = Diff<string | number | (() => void), Function>; // string | number type T33 = Filter<string | number | (() => void), Function>; // () => void type NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from T type T34 = NonNullable<string | number | undefined>; // string | number type T35 = NonNullable<string | string[] | null | undefined>; // string | string[] function f1<T>(x: T, y: NonNullable<T>) { x = y; // Ok y = x; // Error } function f2<T extends string | undefined>(x: T, y: NonNullable<T>) { x = y; // Ok y = x; // Error let s1: string = x; // Error let s2: string = y; // Ok }
Conditional types are particularly useful when combined with mapped types:
tstype FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never; }[keyof T]; type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>; type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K; }[keyof T]; type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>; interface Part { id: number; name: string; subparts: Part[]; updatePart(newName: string): void; } type T40 = FunctionPropertyNames<Part>; // "updatePart" type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts" type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void } 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
tstype 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:
tstype 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:
tstype Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise<infer U> ? U : T; type T0 = Unpacked<string>; // string type T1 = Unpacked<string[]>; // string type T2 = Unpacked<() => string>; // string type T3 = Unpacked<Promise<string>>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> 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:
tstype Foo<T> = T extends { a: infer U; b: infer U } ? U : never; type T10 = Foo<{ a: string; b: string }>; // string 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:
tstype Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never; type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string 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.
tsdeclare function foo(x: string): number; declare function foo(x: number): string; declare function foo(x: string | number): string | number; type T30 = ReturnType<typeof foo>; // string | number
It is not possible to use infer
declarations in constraint clauses for regular type parameters:
tstype 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:
tstype AnyFunction = (...args: any[]) => any; 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 fromT
those types that are assignable toU
.Extract<T, U>
— Extract fromT
those types that are assignable toU
.NonNullable<T>
— Excludenull
andundefined
fromT
.ReturnType<T>
— Obtain the return type of a function type.InstanceType<T>
— Obtain the instance type of a constructor function type.
Example
tstype T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T02 = Exclude<string | number | (() => void), Function>; // string | number type T03 = Extract<string | number | (() => void), Function>; // () => void type T04 = NonNullable<string | number | undefined>; // string | number type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[] function f1(s: string) { return { a: 1, b: s }; } class C { x = 0; y = 0; } type T10 = ReturnType<() => string>; // string type T11 = ReturnType<(s: string) => void>; // void type T12 = ReturnType<<T>() => T>; // {} type T13 = ReturnType<<T extends U, U extends number[]>() => T>; // number[] type T14 = ReturnType<typeof f1>; // { a: number, b: string } type T15 = ReturnType<any>; // any type T16 = ReturnType<never>; // any type T17 = ReturnType<string>; // Error type T18 = ReturnType<Function>; // Error type T20 = InstanceType<typeof C>; // C type T21 = InstanceType<any>; // any type T22 = InstanceType<never>; // any type T23 = InstanceType<string>; // Error type T24 = InstanceType<Function>; // Error
Note: The
Exclude
type is a proper implementation of theDiff
type suggested here. We’ve used the nameExclude
to avoid breaking existing code that defines aDiff
, plus we feel that name better conveys the semantics of the type. We did not include theOmit<T, K>
type because it is trivially written asPick<T, Exclude<keyof T, K>>
.
Improved control over mapped type modifiers
Mapped types support adding a readonly
or ?
modifier to a mapped property, but they did not provide support the ability to remove modifiers.
This matters in homomorphic mapped types which by default preserve the modifiers of the underlying type.
TypeScript 2.8 adds the ability for a mapped type to either add or remove a particular modifier.
Specifically, a readonly
or ?
property modifier in a mapped type can now be prefixed with either +
or -
to indicate that the modifier should be added or removed.
Example
tstype MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] }; // Remove readonly and ? type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] }; // Add readonly and ?
A modifier with no +
or -
prefix is the same as a modifier with a +
prefix. So, the ReadonlyPartial<T>
type above corresponds to
tstype ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] }; // Add readonly and ?
Using this ability, lib.d.ts
now has a new Required<T>
type.
This type strips ?
modifiers from all properties of T
, thus making all properties required.
Example
tstype Required<T> = { [P in keyof T]-?: T[P] };
Note that in --strictNullChecks
mode, when a homomorphic mapped type removes a ?
modifier from a property in the underlying type it also removes undefined
from the type of that property:
Example
tstype Foo = { a?: string }; // Same as { a?: string | undefined } type Bar = Required<Foo>; // Same as { a: string }
Improved keyof
with intersection types
With TypeScript 2.8 keyof
applied to an intersection type is transformed to a union of keyof
applied to each intersection constituent.
In other words, types of the form keyof (A & B)
are transformed to be keyof A | keyof B
.
This change should address inconsistencies with inference from keyof
expressions.
Example
tstype A = { a: string }; type B = { b: string }; type T1 = keyof (A & B); // "a" | "b" type T2<T> = keyof (T & B); // keyof T | "b" type T3<U> = keyof (A & U); // "a" | keyof U type T4<T, U> = keyof (T & U); // keyof T | keyof U type T5 = T2<A>; // "a" | "b" type T6 = T3<B>; // "a" | "b" type T7 = T4<A, B>; // "a" | "b"
Better handling for namespace patterns in .js
files
TypeScript 2.8 adds support for understanding more namespace patterns in .js
files.
Empty object literals declarations on top level, just like functions and classes, are now recognized as as namespace declarations in JavaScript.
jsvar ns = {}; // recognized as a declaration for a namespace `ns` ns.constant = 1; // recognized as a declaration for var `constant`
Assignments at the top-level should behave the same way; in other words, a var
or const
declaration is not required.
jsapp = {}; // does NOT need to be `var app = {}` app.C = class {}; app.f = function() {}; app.prop = 1;
IIFEs as namespace declarations
An IIFE returning a function, class or empty object literal, is also recognized as a namespace:
jsvar C = (function() { function C(n) { this.p = n; } return C; })(); C.staticProperty = 1;
Defaulted declarations
“Defaulted declarations” allow initializers that reference the declared name in the left side of a logical or:
jsmy = window.my || {}; my.app = my.app || {};
Prototype assignment
You can assign an object literal directly to the prototype property. Individual prototype assignments still work too:
tsvar C = function(p) { this.p = p; }; C.prototype = { m() { console.log(this.p); } }; C.prototype.q = function(r) { return this.p === r; };
Nested and merged declarations
Nesting works to any level now, and merges correctly across files. Previously neither was the case.
jsvar app = window.app || {}; app.C = class {};
Per-file JSX factories
TypeScript 2.8 adds support for a per-file configurable JSX factory name using @jsx dom
pragma.
JSX factory can be configured for a compilation using --jsxFactory
(default is React.createElement
). With TypeScript 2.8 you can override this on a per-file-basis by adding a comment to the beginning of the file.
Example
ts/** @jsx dom */ import { dom } from "./renderer"; <h></h>;
Generates:
jsvar renderer_1 = require("./renderer"); renderer_1.dom("h", null);
Locally scoped JSX namespaces
JSX type checking is driven by definitions in a JSX namespace, for instance JSX.Element
for the type of a JSX element, and JSX.IntrinsicElements
for built-in elements.
Before TypeScript 2.8 the JSX
namespace was expected to be in the global namespace, and thus only allowing one to be defined in a project.
Starting with TypeScript 2.8 the JSX
namespace will be looked under the jsxNamespace
(e.g. React
) allowing for multiple jsx factories in one compilation.
For backward compatibility the global JSX
namespace is used as a fallback if none was defined on the factory function.
Combined with the per-file @jsx
pragma, each file can have a different JSX factory.
New --emitDeclarationOnly
--emitDeclarationOnly
allows for only generating declaration files; .js
/.jsx
output generation will be skipped with this flag. The flag is useful when the .js
output generation is handled by a different transpiler like Babel.