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:
- A string literal type is a subtype of and assignable to type
string
. - A numeric literal type is a subtype of and assignable to type
number
. - The boolean literal types
true
andfalse
are subtypes of and assignable to typeboolean
. - A union enum member type is a subtype of and assignable to the containing enum type.
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:
- Operands of the
===
,!==
,==
, and!=
operators. - An expression in a
case
clause of aswitch
statement. - An expression within parentheses in a literal type location.
- The true and false expressions of a conditional operator (
?:
) in a literal type location. - An expression that is contextually typed by a literal type or a union of one or more literal types.
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.