New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Number, enum, and boolean literal types #9407

Merged
merged 58 commits into from Jul 28, 2016

Conversation

Projects
None yet
10 participants
@ahejlsberg
Member

ahejlsberg commented Jun 29, 2016

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 and false are subtypes of and assignable to type boolean.
  • 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 a switch 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<T> = { success: true, value: T } | { success: false };

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

function unwrap<T>(x: Result<T>) {
    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.

@Eyas

This comment has been minimized.

Contributor

Eyas commented Jun 29, 2016

What about

("foo" === "bar"); // is this an error in this case, since a literal
                   // is inferred as a literal type at that location?

These are similar to the "this branch is always true"/false warnings you'd get in some languages (which we now have more of with --stictNullChecks I believe(?)). I'm still torn about whether they're errors or warnings..

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 29, 2016

@Eyas Yes, "foo" === "bar" is an error since the two string literals have incompatible literal types. This error is effectively the mechanism for detecting misspelled string literals. For example, see discussion in #6149.

@weswigham

This comment has been minimized.

Member

weswigham commented Jun 30, 2016

Comments based on my experience testing my PR (which you should steal tests from):

I don't see any NaN or (+/-)Infinity numeric types - should they be there? NaN at least probably should - and it should be one of the Falsy types (and needs to be handled like 0). NaN also potentially needs to have some really funky and unique comparability rules - as NaN === NaN is false. Additionally, parsing -Infinity in a type position is likely to throw a wrench in the current literal type parsing scheme.

Are equivalent numbers with different text equivalent, eg 0x1 vs 1 vs 0o1 vs 01 (octal literals are super fun), and do they preserve their original text? I don't see anything in the type relationship arithmetic to account for this textual inequality (yet value equality).

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 30, 2016

@weswigham Unless someone has a compelling scenario, I'm strongly inclined not to include NaN and Infinity as literal types. Literal types intended for scenarios where you pick a (small) set of specific values to match on in type guards and overloads, and I simply can't imagine meaningful uses of NaN or Infinity as a discriminant values or literal values for overloading.

Yes, 1, 0x1, 1.0, and 1e0 are all the same type, but the canonical representation is always used when displaying types (i.e. the string you get by converting the number to text using "" + value).

I'll look at poaching some tests.

@ahejlsberg ahejlsberg referenced this pull request Jul 3, 2016

Closed

Union Types And Strings #9489

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jul 28, 2016

The RWC baseline differences look good except for a few cases where our optimistic control flow analysis is fooled by hidden side effects in function calls. We're discussing that in #9998. Otherwise I think we're finally ready to merge this PR.

@ahejlsberg ahejlsberg merged commit 0c131fa into master Jul 28, 2016

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@aleksey-bykov

This comment has been minimized.

aleksey-bykov commented Jul 28, 2016

🎉 thank you for the hard work!

@kitsonk

This comment has been minimized.

Contributor

kitsonk commented Aug 29, 2016

@mhegazy this doesn't have a milestone and isn't listed on the Roadmap... Is this going into TypeScript 2.0 or do we have to wait until TypeScript 2.1?

@mhegazy mhegazy added this to the TypeScript 2.0.1 milestone Aug 29, 2016

@mhegazy mhegazy deleted the literalTypes branch Aug 29, 2016

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Aug 29, 2016

it is a 2.0-RC feature. added milestone. and updated the roadmap.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.