Skip to content

Instantly share code, notes, and snippets.

@DanielRosenwasser
Last active January 21, 2016 21:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielRosenwasser/57f111bcb5f52457cc6d to your computer and use it in GitHub Desktop.
Save DanielRosenwasser/57f111bcb5f52457cc6d to your computer and use it in GitHub Desktop.
Which strategy do we take with literal types?

Motivation

Originally we used contextual types to decide when to infer a string literal type. If a string literal had a contextual type, we'd create a literal type for the string. That was the only change.

We ran into two basic problems:

  1. When you want to infer a string literal type for a variable, you always have to write a type annotation.

    var x: "hello" = "hello";

    This is especially silly for const declarations.

    const x: "hello" = "hello";

    We considered inferring literal types for consts, but that would be a breaking change.

    // Imagine 'x' has the type '"hello"'
    const x = "hello";
    let y = x;
    
    // Now this errors.
    y = "bye";
  2. Comparisons were too strict.

    var x: "foo" | "bar" | Refrigerator;
    
    // This comparison fails. "foo" has the type 'string',
    // which isn't assignable to '"foo" | "bar" | Refrigerator'.
    if (x === "foo") {
        // ...
    }

    One idea was to use a new type relationship, but this was not adopted. We instead decided to make it so that anything string-ish could be compared with any other string-ish thing. This is the current behavior in nightlies, but it's too lax:

    var x: "foo" | "bar" | Refrigerator;
    
    // This is considered okay.
    switch (x) {
        case "blahblah":
            // ...
            break;
    }

    The compiler logic is also very ad-hoc, and not at all tied in with other type relations.

Strategies

There are two alternative strategies.

Infer literal types at comparison locations

microsoft/TypeScript#6196

The idea here that when we need to use a string literal for a comparison, we will infer a literal type. In a sense, it's almost like an augmentation of contextual typing. This approach is the least invasive. Its complexity is relatively low, and it affects very few other parts of the type system.

However, it's a new concept that we'd have to think about, separate from contextual typing and widening.

This solves problem 2, but doesn't do much for problem 1.

Always infer string literal types, widen as necessary

microsoft/TypeScript#6554

This is a much broader change. With this strategy, a string literal always starts off with a fresh string literal type. Then, depending on if we are binding to a mutable or immutable variable, we widen as we see fit so that the user can effectively use that variable.

So for instance, in this example,

const c = "foo";

c effectively just holds onto the type "foo" from the string, whereas in this example,

var v = "foo";

the type "foo" is widened to string because v is a mutable binding.

The complexity is somewhat high here. We have

  1. A new concept of "mutability widening" and new work within widening in general.

  2. The concept of "freshness" on string literals.

  3. More difficulty in reasoning the types of variables following assignment.

    In the following example, b and c have different types.

    const a = "hello";
    let b = a;
    const c = a;
  4. Creating two new types for every string literal seems a little excessive.

But this solves both problem 1 and 2 pretty effectively.

Open questions

  1. Can we take one approach and consider the other later on? If we used the "literal type locations" approach, we could always switch over to the widening approach since it seems the former is more conservative.

  2. Is there a combined approach? We could include const as a literal type location, and then perform widening appropriately at mutable binding points. I believe freshness could be eliminated because contextual typing will sort out the issues that matter.

  3. Is problem 1 actually a problem? Who needs to write that? The most convincing case I was able to think of was a makeshift enum:

    namespace Syntax {
        export const IfStatement = "IfStatement";
        export const PropertyAccess = "PropertyAccess";
        export const ElementAccess = "ElementAccess";
        // ... 
    }

    But this might be the wrong solution to a separate problem: people want enums of types other than number. If we ever plan to truly ship that solution, it is my opinion that we need to rethink whether this is compelling enough right now that it warrants the complexity.

@JsonFreeman
Copy link

I think a local const, meant to alias a string literal in a particular scope, is more compelling than the string enum idea.

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