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

Writing a for-in loop variable with a proper subtype of string should require a type assertion #57288

Closed
danvk opened this issue Feb 4, 2024 · 8 comments
Labels
Duplicate An existing issue was already created

Comments

@danvk
Copy link
Contributor

danvk commented Feb 4, 2024

🔎 Search Terms

  • "for-in" loop keyof type assertion

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgIICEDCyDeBYAKGWOTgC5kQBXAWwCNoBuQk5Oi6+plkhD2hlGYEAvoUIwqIBGGAB7EMhhy5ACjh0+aLAEpcPYgBsIYZAGsKZiAE85MbZmGtlUZKrPJQpTXvxFWvAoAznLGAHSGcgDm6poA2mYAumFgcgBiwAAeEAAmqjo6TiRiouIECMGmGgg5yAC8uOTIAIwANGwUAEztWgDM7TkcEADuyAAicJD5IsLKatU5hcQA9MvIgLwbgKU73ggJyakZ2bXAQZRyVUpSMvKKhEA

💻 Code

interface ABC {
    a: number;
    b: number;
    c: number;
}

function foo(abc: ABC) {
    let k: keyof ABC;
    for (k in abc) {
        console.log(abc[k].toFixed());
    }
}

const abcd = {a: 1, b: 2, c: 3, d: new Date()};
foo(abcd);  // 💥 abc[k].toFixed is not a function 

🙁 Actual behavior

TypeScript allows this code without a type assertion on k.

🙂 Expected behavior

TypeScript should require a type assertion on k, whose type really should be string. Something like:

for (const kStr in abc) {
  const k = kStr as keyof ABC;
}

The code it allows is equivalent to a type assertion but does not use the word "as".

Additional information about the issue

This has been the behavior of TS forever, but I had trouble finding an issue requesting that TS be more strict about this, rather than less.

If you want to shoot yourself in the foot, you should at least have to write a type assertion somewhere.

Incidentally, the error you get if you write let k: "a" | "b" is a bit misleading given the behavior:

function foo(abc: ABC) {
    let k: "a" | "b";
    for (k in abc) {
    //   ~ The left-hand side of a 'for...in' statement must be of type 'string' or 'any'. (2405)
        console.log(abc[k].toFixed());
    }
}

string would be sound, but evidently TS only requires something that keyof ABC is assignable to, not string.

@fatcerberus
Copy link

You're like the first person I've seen in forever advocating that the key should be typed string. Everyone else complains that for (const k in abc) is typed string and instead wants it typed as keyof typeof abc (likewise for Object.keys(abc)). And then when you argue with them, they go "but the excess property check tho" 😛

Anyway, I think this is a duplicate of #49882, which links to #3500 and suggests that this might be intentional.

@danvk
Copy link
Contributor Author

danvk commented Feb 4, 2024

Thanks for that issue. This is definitely a dupe of #49882, though I think the title on this one is much clearer ("incorrect type using keyof and for..in" sounds like just another issue requesting the unsound behavior).

@Andarist
Copy link
Contributor

Andarist commented Feb 5, 2024

You're like the first person I've seen in forever advocating that the key should be typed string

Not quite :P see here (where you also left some comments in the past 😅 )

@ssalbdivad
Copy link

I also agree the variable should be typed as string but really hope they do not remove the let escape hatch before we have a real solution with exact types 😄

@danvk
Copy link
Contributor Author

danvk commented Feb 5, 2024

@Andarist -- wow, you have a PR for everything! :)

@ssalbdivad the "escape hatch" would be an explicit type assertion:

for (const kStr in abc) {
  const k = kStr as keyof ABC;
}

One more variable than the current form, yes, but the same number of lines of code and it's at least more honest about what's going on.

let k: keyof ABC;
for (k in abc) {
}

@Andarist
Copy link
Contributor

Andarist commented Feb 5, 2024

@Andarist -- wow, you have a PR for everything! :)

I do my best 😉

@ssalbdivad the "escape hatch" would be an explicit type assertion:

I also think that this is an acceptable escape hatch (the cast I mean). It's at least honest about its nature so the reader can understand better what happens here.

@ssalbdivad
Copy link

ssalbdivad commented Feb 5, 2024

@danvk @Andarist It's obviously subjective, but the first feels much worse to me:

  1. You have to come up with a second variable name
  2. You're no longer being able to see key reference you should use in the loop declaration
  3. Easy if you're not careful to reference the original key declaration (which is still in scope) and get confusing errors, especially when revisiting old code

It also feels like the upside is very limited:

  • It would be very hard to run into the current let behavior by accident
  • There are already many other cases where TypeScript will narrow in ways that aren't safe in a structural type system e.g.:
type A = {
	a: true
	isA: true
}

type B = {
	b: true
	isB: true
}

const aOrB: A | B = {
	a: true,
	b: true,
	isB: true
}

if ("a" in aOrB) {
	// narrowed to A
	aOrB.isA
	//   ^?
}

Calling this behavior an anomaly in the first place is a bit of a stretch because it's an anomaly consistent with many other existing "anomalies" which are clearly behaving as intended.

That said, barring the implementation of exact types, I'd be fine with the let behavior being removed if an inline cast syntax like this were supported, as it would address all of my problems with the second variable workaround:

for (const k as keyof ABC in abc) {
...
}

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Feb 5, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants