Skip to content
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

Const primitive value type getting lost? #48402

Closed
TeoTN opened this issue Mar 24, 2022 · 11 comments
Closed

Const primitive value type getting lost? #48402

TeoTN opened this issue Mar 24, 2022 · 11 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@TeoTN
Copy link

TeoTN commented Mar 24, 2022

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

@DuncanWalter
Copy link

DuncanWalter commented Mar 24, 2022

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'

@whzx5byb
Copy link

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.

@TeoTN
Copy link
Author

TeoTN commented Mar 24, 2022

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

@fatcerberus
Copy link

fatcerberus commented Mar 24, 2022

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.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Mar 25, 2022
@typescript-bot
Copy link
Collaborator

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.

@TeoTN
Copy link
Author

TeoTN commented Mar 28, 2022

@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.

@RyanCavanaugh
Copy link
Member

@TeoTN the difference in behavior, which is intentional, was correctly described by @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).

@fatcerberus
Copy link

@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.

@RyanCavanaugh
Copy link
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.

@fatcerberus
Copy link

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.

@RyanCavanaugh
Copy link
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants