Number, enum, and boolean literal types by ahejlsberg · Pull Request #9407 · microsoft/TypeScript (original) (raw)

This PR expands upon #9163 by implementing number, enum, and boolean literal types and adding more comprehensive checking and type guard constructs for literal types. The PR supercedes #6196 and #7480, and fixes #2983, #6149, #6155, #7447, and #7642.

It is now possible to use string, numeric, and boolean literals (true and false) as literal types:

type Direction = -1 | 0 | 1; type TrueOrFalse = true | false; type Falsy = "" | 0 | false | null | undefined;

The predefined boolean type is now equivalent to the union type true | false.

When each member of an enum type has either an automatically assigned value, an initializer that specifies a numeric literal, or an initializer that specifies a single identifier naming another enum member, that enum type is considered a union enum type. The members of a union enum type can be used both as constants and as types, and the enum type is equivalent to a union of the declared member types.

const enum ShapeKind { Square, Rectangle, Circle }

type Shape = { kind: ShapeKind.Square; size: number; } | { kind: ShapeKind.Rectangle; width: number; height: number; } | { kind: ShapeKind.Circle; radius: number; };

All literal types as well as the null and undefined types are considered unit types. A unit type is a type that has only a single value.

Literal types have the following type relationships:

Certain expression locations are now considered literal type locations. In a literal type location, a literal expression has a literal type (e.g. "hello", 0, true, ShapeKind.Circle) instead of a regular type (e.g. string, number, boolean, ShapeKind). The following expression locations are considered literal type locations:

When both operands of ===, !==, ==, or != are unit types or unions of unit types, the operands are checked using those types and an error is reported if the types are not comparable. Otherwise the operands are checked with their full types.

const foo: "foo" = "foo"; const bar: "bar" = "bar"; let s: string = "abc"; foo === bar; // Error, "foo" and "bar" not comparable foo === s; // Ok bar === s; // Ok

The equality comparison operators now narrow each operand based on the type of the other operand. For example:

function f1(x: "foo" | "bar" | "baz") { if (x === "foo" || x === "bar") { x; // "foo" | "bar" } else { x; // "baz" } }

function f2(x: string | boolean | null, y: string | number) { if (x === y) { x; // string y; // string } else { x; // string | boolean | null y; // string | number } }

When all case expressions in a switch statement have unit types, the switch expression variable is narrowed in each case block and in the default block based on the listed cases. For example:

function f3(x: 0 | 1 | 2 | 3) { switch (x) { case 0: x; // Type of x is 0 break; case 1: case 2: x; // Type of x is 1 | 2 break; default: x; // Type of x is 3 } }

For an equality, truthiness, or switch type guard on a reference x.y, we not only narrow x.y but also narrow x itself based on the narrowed type of x.y. For example:

type Result = { success: true, value: T } | { success: false };

function foo(): Result { if (someTest) { return { success: true, value: 42 }; } else { return { success: false }; } }

function unwrap(x: Result) { switch (x.success) { case true: return x.value; case false: throw new Error("Missing value"); } }

let x = foo(); let y1 = x.success === true ? x.value : -1; // Type guard allows x.value to be accessed let y2 = !x.success ? -1 : x.value; let y3 = x.success && x.value || -1; let y4 = unwrap(x);

The && and || operators now understand that 0, false, and "" are falsy unit types and stricter typing of these operators is implemented as described here.