-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Type for-in initializer as string
#54856
base: main
Are you sure you want to change the base?
Conversation
This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise. |
let k1: K; | ||
for (k1 in obj) { | ||
~~ | ||
!!! error TS2405: The left-hand side of a 'for...in' statement must be of type 'string' or 'any'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an error because this is a valid call:
declare const obj: { a: 1, b: 2, c: 3 }
declare const key: 'a' | 'b'
f1(obj, key)
'c'
is not assignable to K
These two statements contradict. |
string
string
Good catch :p |
The statement that the for-in initializer must be a string and cannot be of a stricter type is contradictory. If the initializer is restricted to a string it would limit its usefulness as objects can have keys of different types. Therefore, it is appropriate that the for-in initializer accepts broader types such as string and any to cover all possible object keys. |
@Mateusnasciment "Initializer" is a misleading term here IMO - this is specifically about the type of the range variable of According to MDN:
|
@Andarist IIRC @RyanCavanaugh has said in the past that this was an intentional soundness tradeoff so people could do this... const obj = { foo: 777, bar: 1206 };
let key: keyof typeof obj;
for (key in obj) {
/* key :: "foo" | "bar" */
console.log(obj[key]); // normally an error, now ok
} ...otherwise you have to type-assert the key every time. |
Ye, I get what is the consequence of that. However, I really can’t find a reason why this would not be consistent with |
@RyanCavanaugh If this is true, thank you- it is a massive help! From a discussion in TS Discord:
If I understand correctly that this change would break that workaround, I'm strongly in favor of preserving the existing behavior. As far as I'm aware, without it, you're essentially forced to declare an extra variable in the loop body just to narrow the key type. If you're adding runtime logic just to make your types happy, something has gone wrong- a cast should always be sufficient in these situations. |
Aside: TIL |
Enumerable ones but ye - it has been a popular combo in the past to have for-in+hasOwnProperty |
The reason const arr = [1, 2, 3, 4];
for (const i in arr) {
console.log(arr[i]);
} Rather than give Should we just give up and type |
I probably wouldn't cal this idiomatic in today's landscape so perhaps if that was the only-ish reason for this behavior then it could be revisited.
It could be given a type of
I totally get how you might be tired of this. I'm glad that |
The problem with that is you would get the wrong type as a result of subclassing: interface Point { x: number, y: number }
interface Point3D extends Point { z: number };
const p2d: Point = { x: 0, y: 1 };
const p3d: Point3D = { ...p2d, z: 2 };
let p: Point;
p = p2d;
Object.keys(p); // ("x" | "y")[]
p = p3d;
Object.keys(p); // ("x" | "y")[], but it also contains "z" The wider type is safer. |
@RyanCavanaugh I agree with @Andarist and @rbuckton (and you) that the widened type makes sense. It feels like the only good solution to this is an exact constraint on object types that allows narrowed key iteration and disallows assignment from non-exact objects. |
I think that for-in should have the same soundness guarantees as
Object.keys
. SinceObject.keys
always returnstring[]
, the same should be true for the for-in initializer - it has to be astring
and can't be of a more narrow type.