-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Solve the for-in / indexer mess #6346
Comments
If there was some way of communicating that your string type was actually the domain of strings whose text was numeric (i.e. ToString(ToNumber(text) === text), that would possibly go a long way here. |
I had an offline discussion with @ahejlsberg about this. I think we have a proposal. given: for (var x in a) {...}
Where var a = [1, 2, 3];
for (var x in a) {
y = a[x]; // y is number
z = x + 1 // z is string;
} |
An alternative is to check whether the expression used to index an array is exactly an identifier declared as a for-in variable for an array that has a numeric indexer. This avoids having a new type and, importantly, avoids having to "erase" that type when it becomes in inferred type of a variable. I will try to work on it and put up a PR. |
You mean if it only has a numeric index signature? Or even if it has a string index signature as well? |
@DanielRosenwasser you are correct. it has to have only a numeric index signature. |
An object can satisfy a numeric index signature type and still have other non-numeric properties: var obj: {[n: number]: string};
obj = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
for (var k in obj) {
console.log(k, obj[k].toUpperCase()); // This throws when k === 'length'
} Even on a plain array, for/in may include properties that have been added to |
@jeffreymorlan Perhaps a better check is to see if the object being for-in'ed is array-like (specifically, if it is assignable to |
One library I'm using returns array-like objects that inherit from Array.prototype but add additional (enumerable) methods that will show up in for/in, making it unsafe to use the for/in keys as indexes. There are so many ways that for/in on array-likes may return properties other than array indexes...
...not to mention the other surprising behaviors (indexes are strings instead of numbers, indexes not guaranteed to be in numeric order pre-ES6), that for/in on an array-like should just be considered a bug. At any rate it shouldn't be given special treatment from the type system to pretend that it's safe. |
@jeffreymorlan Completely agree for-in is a messed up construct--but we don't get to change that. The objective is to provide the best possible type checking of for-in as it exists, and unquestionably it is a common pattern to use for-in with arrays and expect indexing with the for-in variable to produce the elements of the array. I agree that there are situations where non-numeric property names may appear because applications or libraries have added extra properties to arrays, but we have to balance that against existing (and more common) code that just uses plain JavaScript arrays. The operating principle here is to find the best possible compromise. I think the changes in #6379 bring us closer to that. |
It's a common mistake; all the more reason not to allow it in TS, so people can find places where they have made that mistake and replace it with the correct for/of, .forEach, or good-old-fashioned Monkey patching of Array.prototype isn't just something obscure that you'll never see in the real world (unfortunately). According to http://w3techs.com/technologies/overview/javascript_library/all MooTools is used by 3.8% of websites, and Prototype by 2.2%. It's a controversial practice; I don't like it myself, but many people take the moderate position that it's fine for polyfilling newer methods like .filter for older browsers - which still breaks for/in. On the other hand, for/in on arrays is nearly universally castigated; google for "for in array" and results # 1, 3, 4, and 7 are all about why you shouldn't do it. Even TypeScript itself used to add properties to Array.prototype that would globally break for/in on arrays. From TypeScript 1.3 src/services/syntax/syntaxList.ts: module TypeScript.Syntax {
...
Array.prototype.kind = function () { ... }
Array.prototype.separatorCount = function (): number { ... }
Array.prototype.separatorAt = function (index: number): ISyntaxToken { ... }
...
} |
There are two questions here:
If people want to disallow a construct entirely, there are good tools for that -- TSLint, ESHint, etc.. I know there are tools that will disallow If people think they're in an environment where Our goal has always been to not be more prescriptive than we have to be. We regularly get issues filed here asking us to be more permissive of things that are much more error-prone (e.g. overloading |
@RyanCavanaugh I'm certainly not asking for for/in to be disallowed - I use it all the time myself - all I'm asking is that it shouldn't bend the indexing rules to make it look like it's safer for arrays than it actually is: var map: {[key: string]: string};
for (var key in map) { // good
// should be allowed, map has a string index signature
console.log(map[key].toUpperCase());
}
var arr: string[];
for (var key in arr) { // probably a mistake; key is a string but you can't assume much else about it
// this should be an error, because arr does not have a string index signature
console.log(arr[key].toUpperCase());
} |
We bent the rules pretty severely by making the A surprising thing we've discovered in practice is that people strongly disagree about what indexing behavior should be considered "safe" or not -- see the fact that the Of course, the real lie here is the existence of numeric index signatures at all, but that cat is out of the bag (and never really fit in the bag in the first place). This change straightens the rules a bit so that the really egregious stuff ( |
It's an easy fix to change Converting any nontrivial JS program to error-free TS, even without --noImplicitAny, already requires much larger changes. Here's one that I've encountered a lot: var o = {};
if (foo) {
o.x = ...; // Error: Property 'x' does not exist on type {}
} else {
o.x = ...; // Error: Property 'x' does not exist on type {}
}
return o; That's a common JS pattern and not even a dangerous one. But it wasn't considered important enough to have special, confusing properties-set-outside-the-object-literal rules made for it. You have to either declare Compared to that, fixing array for/ins both is easier, and is actually making bad code better (as opposed to just making already acceptable JS code more type-checkable). |
I didn't read the full thread, but wanted to say that this change in TS 1.8 broke several places in our codebase when upgrading to the beta. it seems @ahejlsberg has special-cased the For instance, from our codebase: for (let i in someArray) {
// ... stuff
someArray.splice(i, 1);
} It fails because |
This is very much intentional. Consider if the Indexed access doesn't have this problem. |
@RyanCavanaugh Interesting, I was frustrated by the errors and didn't think it through. Clearly you are right and allowing all For the Maybe you could special case conversions to |
Shouldn't this work ? interface A {
[index:number]:number;
}
var a : A = {1:1,2:2};
for (var x in a) {
var y = x * 2;
} |
@nn look three comments above.
As of today, TS has a built-in exception whereas if Note that in your code above, TS is correct assuming var a = {1: 1, 2: 2};
for (var x in a)
console.log(typeof x); And you'll see |
@jods4 I understand and agree but I would like to have some 'string_index_as_number' type which allows only to be used as indexer and disallow everything else. |
interface A {
[index:number]:number;
}
var a : A = {1:1,2:2};
for (var x in a) {
var y = x + 2; // y: '12', '22'
} |
@RyanCavanaugh I get your point. I have something like this: interface SubStates {
[subStateId: number]: SubState;
}
class State {
subStates: SubStates = {};
}
interface States {
[id: number]: State;
}
// Map like object
states: States = {};
// Recursive traversal
function f(id: number) {
var subStates = states[id].subState;
if (subStates) {
for (var subState in subStates) {
f(subState);
}
}
} Since I used the 'id' only as indexer it worked without any problem. |
I don't understand what doesn't work about your sample. |
The issue is here are the options:
|
Sorry, my bad. |
the last would be my recommendation, as it has no runtime impact (vs |
@mhegazy Good idea. I totally forgot that I can put a variable outside of the for loop. |
if |
Thank you all. |
/cc @vladima @ahejlsberg @billti
Motivating Examples
This should be an error (
k
is always astring
):This should not be an error, even under no implicit any:
This should somehow? not be an error, even though it involves a coercion we would normally disallow
The text was updated successfully, but these errors were encountered: