Skip to content

Inconsistent String Literal Type Propagation #15271

Closed

Description

TypeScript Version: 2.3.0 RC

I'm observing some strange type behavior. Consider the following snippet.

type myType = "MY_TYPE";
const myType: myType = "MY_TYPE";

const foo = myType;
const bar = {
  baz: foo
};

The type of foo and baz are both "MY_TYPE" as expected.

However using this strEnum function as described here which, based on my understanding, should be doing the same thing as the above code, results in something different:

function strEnum<T extends string>(o: Array<T>): {[K in T]: K} {
  return o.reduce((res, key) => {
    res[key] = key;
    return res;
  }, Object.create(null));
}

const myTypeEnum = strEnum(["MY_TYPE"])

const foo = myTypeEnum.MY_TYPE;
const bar = {
  baz: foo
};

Here, foo is detected to be type "MY_TYPE", but baz is recognized as type string

Am I doing something wrong? Is this expected behavior?

Activity

  1. ahejlsberg commented on Apr 19, 2017

    @ahejlsberg
    Member

    This is working as intended. In the call strEnum(["MY_TYPE"]), the "MY_TYPE" literal has a widening literal type (see #11126) because it was never given an explicit type through a type annotation or a type assertion. You get the desired result if you explicitly assert a type:

    const myTypeEnum = strEnum(["MY_TYPE" as "MY_TYPE"])
  2. bilalq commented on Apr 20, 2017

    @bilalq

    I'm left a bit surprised by the type widening here. In a project using react-redux, I'm having to declare types that I would expect the compiler to be able to infer.

    In all of the following examples, I would expect the code to compile. However, there are many where it does not:

    // Example 1
    // DOES NOT compile
    interface MyAction {
      type: 'MY_ACTION'
    }
    const action = {
      type: 'MY_ACTION'
    }
    const reassignment: MyAction = action;
    
    
    // Example 2
    // DOES NOT compile
    const MY_ACTION = 'MY_ACTION';
    interface MyAction {
      type: typeof MY_ACTION
    }
    const action = {
      type: MY_ACTION
    }
    const reassignment: MyAction = action;
    
    
    // Example 3
    // DOES compile
    const MY_ACTION: 'MY_ACTION' = 'MY_ACTION';
    interface MyAction {
      type: typeof MY_ACTION
    }
    const action = {
      type: MY_ACTION
    }
    const reassignment: MyAction = action;
    
    
    // Example 4
    // DOES compile
    const MY_ACTION = 'MY_ACTION';
    interface MyAction {
      type: typeof MY_ACTION
    }
    const action: MyAction = {
      type: MY_ACTION
    }
    const reassignment: MyAction = action;

    Are all these situations truly as intended? Is this because the object is not guaranteed to be immutable? If that's the case, the widening difference between example 2 and 3 is throwing me off.

  3. RyanCavanaugh commented on Apr 20, 2017

    @RyanCavanaugh
    Member

    Are all these situations truly as intended?

    Consider a slight variant:

    const DEFAULT_COLOR = 'RED';
    const style = {
      background: DEFAULT_COLOR;
    };
    if (user.useNeatBackground) {
      // Error? Cannot convert "BLUE" to "RED" ?
      style.background = 'BLUE';
    }

    Without explicit intent, it's very tricky to infer literal types. You can quickly find yourself issuing errors on code that is extremely unsuspicious.

  4. mattnedrich commented on Apr 20, 2017

    @mattnedrich
    Author

    Thanks for the clarification! I'm going to close.

  5. bilalq commented on Apr 20, 2017

    @bilalq

    Hmm... that case certainly makes sense. I don't know if it's prohibitively expensive to do this, but could the compiler avoid widening the type within a certain scope if it can guarantee that an object is never mutated? Though even in that case

    Another possible alternative here that might be nice would be to introduce a keyword in the type layer called literal.

    const SUBMIT_COMMENT_SUCCEEDED: literal = 'SUBMIT_COMMENT_SUCCEEDED';

    It expresses intent clearly, and avoids the triple repetition that we currently have to do:

    const SUBMIT_COMMENT_SUCCEEDED: 'SUBMIT_COMMENT_SUCCEEDED' = 'SUBMIT_COMMENT_SUCCEEDED';

    Would this be a proposal that might be considered? Or are there downsides to this?

  6. locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions