Skip to content

Const primitive value type getting lost? #48402

Closed

Description

@TeoTN

Bug Report

It seems to me that type for primitive value assigned to a const variable gets widened in some contexts - I'm not 100% sure if this is a bug report or feature request, but this is a bit counterintuitive. Is this an expected behaviour?
(From what I saw in the playground hints, primitive values assigned to const behave as if they had as const modifier)

🔎 Search Terms

primitive value
primitive widening

⏯ Playground Link

Playground link with relevant code

💻 Code

The hint shows the TYPE_1 is not string:
Screenshot 2022-03-24 at 18 04 34

const TYPE_1 = 'TYPE_1';
const TYPE_2 = 'TYPE_2';
const TYPE_3 = 'TYPE_3' as const;
const TYPE_4 = 'TYPE_4' as const;

const action1 = () => ({ type: TYPE_1 });
const action2 = () => ({ type: TYPE_2, payload: {} });
const action3 = () => ({ type: TYPE_3 });
const action4 = () => ({ type: TYPE_4, payload: {} });

type NonConstTypeActions =
  | ReturnType<typeof action1>
  | ReturnType<typeof action2>
;

type ConstTypeActions =
  | ReturnType<typeof action3>
  | ReturnType<typeof action4>
;

function nonConstTypesReducer(state: {}, action: NonConstTypeActions) {
  switch (action.type) {
    case TYPE_2:
      return action.payload; // Property 'payload' does not exist on type 'NonConstTypeActions'.
    default:
      return {};
  }
}

function withConstTypesReducer(state: {}, action: ConstTypeActions) {
  switch (action.type) {
    case TYPE_4:
      return action.payload;
    default:
      return {};
  }
}

🙁 Actual behavior

Type guard wasn't able to narrow down a type because field type got widened to string.

🙂 Expected behavior

Both examples should pass

Activity

  1. DuncanWalter commented on Mar 24, 2022

    @DuncanWalter

    Another way to express the issue (I think this is the same issue at least):

    Playground

    const TYPE_1 = 'TYPE_1';
    const TYPE_2 = 'TYPE_2' as const;
    
    const action1 = { type: TYPE_1 };
    const action2 = { type: TYPE_2 };
    
    action1.type = 'string'
    // Why does this throw? It's not a readonly prop, so it should have inferred 'string'
    action2.type = 'string'
  2. whzx5byb commented on Mar 24, 2022

    @whzx5byb

    const TYPE_1 = 'TYPE_1' have a widening type, while const TYPE_2 = 'TYPE_2' as const have a non-widening literal type.

    See #10676, #29510 for more details.

  3. TeoTN commented on Mar 24, 2022

    @TeoTN
    Author

    I'm not 100% sure this is the case, the playground hints that type of const TYPE_1 = 'TYPE_1' is "TYPE_1". This example would suggest that it's not getting widened:

    const TYPE_1 = 'TYPE_1';
    function fn() {
        switch (TYPE_1) {
            case TYPE_1:
            return 4;
            case 'any': // Type '"any"' is not comparable to type '"TYPE_1"'.(2678)
            return 5;
        }
    }
    

    I may not understand correctly what it means for a type to be widening, but it seems a rather inconsistent behaviour if working as intended. I would've expected the type won't change depending on the use case

  4. fatcerberus commented on Mar 24, 2022

    @fatcerberus

    What you're ultimately observing is the difference between:

    const action1 = { type: "foo" };  // { type: string }
    const action2 = { type: "foo" as const };  // { type: "foo" }

    It is a bit odd that the widening flag is apparently being preserved in the type of a const, though. This widening behavior is only supposed to apply to fresh literals.

  5. typescript-bot commented on Mar 28, 2022

    @typescript-bot
    Contributor

    This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.

  6. TeoTN commented on Mar 28, 2022

    @TeoTN
    Author

    Ryan Cavanaugh (@RyanCavanaugh) that felt a bit rough :D It's not a question, and doesn't belong to stackoverflow.
    I don't think it's good that primitive value assigned to const is (allegedly) of literal type value, and then information about the type is lost. I'm not sure if the problem lies in what types are reported (of the const variable) or if type is lost (either is a bug), and I'd probably expect that the type is actually literal for primitives assigned to const since they can't be (meaningfully) reassigned. (Feature request?)

    I'd vastly appreciate if you could kindly reopen it. I'm happy to accept suggestions to issue title/description changes, if they are confusing.

  7. RyanCavanaugh commented on Mar 28, 2022

    @RyanCavanaugh
    Member

    Piotr Staniów (@TeoTN) the difference in behavior, which is intentional, was correctly described by Bruce Pascoe (@fatcerberus) above. There is a difference between the widening and non-widening forms of literals, which isn't displayed in the UI (we discussed this and decided that it would be more confusing, rather than less, on net).

  8. fatcerberus commented on Mar 28, 2022

    @fatcerberus

    Ryan Cavanaugh (@RyanCavanaugh) I find it odd that the “widening literal” flag gets preserved through a const declaration. That’s very confusing since it’s not reflected in any way in the type of the variable. This seems analogous to if object literal freshness were preserved by a const declaration, which would be just as confusing.

  9. RyanCavanaugh commented on Mar 28, 2022

    @RyanCavanaugh
    Member

    The intuition is that this program should not have an error:

    const a = "foo";
    const b = { prop: a };
    b.prop = "bar";

    The widening rules for literals were largely derived empirically, since code like this predates the existence of literal types. One way to think about it is that it should be extremely difficult to get yourself into a literal type mismatch if none of your code contains either an as const or a literal type declaration somewhere in it.

    If I'm misunderstanding your observation let me know; happy to clarify.

  10. fatcerberus commented on Mar 29, 2022

    @fatcerberus

    I guess that makes sense. I think I would rather have const a itself be widened to string (like what happens with let) rather than the potentially confusing deferred widening, but that would be a breaking change at this stage so I won’t suggest it.

  11. RyanCavanaugh commented on Mar 29, 2022

    @RyanCavanaugh
    Member

    We tried the simple thing (both simple things, actually) and they didn't work ergonomically in practice. See discussions at #10898, #11126, and #10676

  12. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions