Skip to content

for .. in type is wrong for objects as const #49621

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

Closed
fabio-colella-md opened this issue Jun 21, 2022 · 13 comments
Closed

for .. in type is wrong for objects as const #49621

fabio-colella-md opened this issue Jun 21, 2022 · 13 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@fabio-colella-md
Copy link

Bug Report

When declaring a new object as const, if we then use a for .. in structure, the type of the key is just inferred as string and not as the more restricted type that const defines. This causes issues especially when trying to use that key to get the corresponding property.

🔎 Search Terms

is:issue for in as const

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about const

⏯ Playground Link

https://www.typescriptlang.org/play?ts=4.8.0-dev.20220621#code/MYewdgzgLgBAtgTwMLmgeQEYCsYF4YDeAsAFAwwCGAXDAERQCm0AjLQDSnkY31NQBMtUgF9KEGKEhQA3KVIAzEACcYACknQYIbAGkGCGAEsw8ZKiiYsASkKdTKKZYDa2rHoQBdEaSA

💻 Code

const myConstObj = {
  a: "test1",
  b: "test2"
} as const;

for (const objKey in myConstObj) {
  myConstObj[objKey]
}/* ^^^^^^^^^^^^^^^^
    const objKey: string
    --------------------
    Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 
    '{ readonly a: "test1"; readonly b: "test2"; }'.
        No index signature with a parameter of type 'string' was found on type 
    '{ readonly a: "test1"; readonly b: "test2"; }'.(7053)
*/

🙁 Actual behavior

When using the key to get the prop, we are told that the prop would have type any because the key is of type string.
Screenshot 2022-06-21 at 16 16 46

🙂 Expected behavior

No error should be reported, as the correct inferred type for the key should be the union of the keys (i.e. 'a' | 'b' in this case) of the const object.

🤔 Workaround

"Coerce" the key type to be what we would have expected, so in this case:

const myConstObj = {
  a: "test1",
  b: "test2"
} as const;

for (const _objKey in myConstObj) {
  const objKey = _objKey as keyof typeof myConstObj
  myConstObj[objKey]
}

Screenshot 2022-06-21 at 16 17 35

@MartinJohns
Copy link
Contributor

Duplicate of #44706.

@fcole90
Copy link

fcole90 commented Jun 21, 2022

This issue is different, as this one is restricted only to objects as const 😉

@MartinJohns
Copy link
Contributor

MartinJohns commented Jun 21, 2022

That makes no difference. Objects are not sealed.

const myConstObj = {
  a: "test1",
  b: "test2"
} as const;

type Data = {
    a: "test1";
    b: "test2";
    c?: number;
}

function foo(data: Data) { data.c = 123; }

foo(myConstObj);

for (const key in myConstObj) {
    // Will print out: a, b, c
    console.log(key);
}

The only case where this would work safely is if the object is used right after it's been declared, like in your example. But that's such a narrow corner case that I doubt the team will support it.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels Jun 21, 2022
@RyanCavanaugh
Copy link
Member

The feature needed to make this OK is exact types. as const changes the type of an expression, it's not something that's reflected in the type system per se. IOW there's nothing from TS's perspective that distinguishes the code in the OP from this code, which would violate the keyof assumption:

const myConstObjTmp = {
  a: "test1",
  b: "test2"
} as const;
const gotcha = { a: "test1", b: "test2", c: "test3" } as const;
const myConstObj: typeof myConstObjTmp = gotcha;

@fabio-colella-md
Copy link
Author

fabio-colella-md commented Jun 22, 2022

I understand your points, indeed there are many valid scenarios where that type I suggested would not be correct 🤔
Hence, I think I should just keep using the workaround. Do you have any suggestions on making that cleaner and more concise? I think it's a very common scenario. I wanted to specify the type from within the for but I couldn't find a way 🤔 At least, I'd like to avoid the need of creating an extra variable 😅

I mean, this doesn't work, but something like this I think would be very useful:

const myConstObj = {
  a: "test1",
  b: "test2"
} as const;

for (const objKey (as keyof typeof myConstObj) in myConstObj) {
  myConstObj[objKey]
}

@fabio-colella-md
Copy link
Author

I'm now also thinking, that you're right that the type I suggested is not correct in many scenarios, however, I still think there might be a narrower type. So, taking @MartinJohns's example, why can't the inferred type be 'a' | 'b' | 'c' instead of a very generic string? 🤔

@MartinJohns
Copy link
Contributor

So, taking @MartinJohns's example, why can't the inferred type be 'a' | 'b' | 'c' instead of a very generic string?

Because the compiler doesn't know there's a property "c". It sees a type that has only two properties: "a" and "b".

@fabio-colella-md
Copy link
Author

Mmh, interesting, thank you everybody for all your explanations 😊 I think this issue can be closed 😊

@fabio-colella-md
Copy link
Author

Alternative workaround

const myConstObj = {
  a: "test1",
  b: "test2"
} as const;

for (const objKey of Object.keys(myConstObj) as (keyof typeof myConstObj)[]) {
  myConstObj[objKey]
}

@MartinJohns
Copy link
Contributor

function getObjectKeysUnsafe<T extends object>(value: T): (keyof T)[] { return Object.keys(value) as (keyof T)[]; }

for (const objKey of getObjectKeysUnsafe(myConstObj)) {
  myConstObj[objKey]
}

@fabio-colella-md
Copy link
Author

Nice, thanks 😊

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Jun 22, 2022
@Darrick-Oliver
Copy link

I found that using a for..of loop fixed my issue

@JosXa
Copy link

JosXa commented May 15, 2024

Here's a gist for those cases where you're sure the object is not modified at runtime: https://gist.github.com/JosXa/a76b4e4cde1ef8ac7092a1dff670aa68

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants