-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Confusing aspects of recent improvements to Index Types #23994
Comments
@ahejlsberg @mhegazy Thoughts? |
Some comments on the comments in the code :)
Any object in JS is indexable with string, that is a basic JS feature, and it is modeled in the TS type system. so these operations are allowed the resulting type however is an implicit I should also add, this is not a new behavior, this has been the behavior since the first TS release.
This should be allowed at some point (tracked by #2049). it is important to note that this is just the same as Any JS index operation has to go through a string coercion process. so at runtime, anything you use to access a property is first translated to
Again, the string indexer is a subset of the number indexer. so |
this is the real issue here. we should disallow that, by looking up the numeric value first. diffTypes["1"] = "ohjoy"; that is technically a breaking change. |
@mhegazy Thanks for the quick reply.
I missed the fact that the resulting type was
Does TS, then, also acknowledge there are objects that we never want to index with number and if we did, it's likely a bug? Because it's not my intent that a number should be used as a key for any of the indexed types that I've ever written. I'd like my objects to be type-checked with |
That alone is insufficient, as such an indexing operation would also need to be somehow forbidden in higher order space; otherwise a simple indirection still exhibits the same issue: interface Obj {
[x: number]: boolean;
[x: string]: boolean | string;
}
function set<T extends keyof Obj>(i: Obj, x: T, v: Obj[T]) {
i[x] = v;
}
const o: Obj = {};
o[1] = true; // OK
const x = set(o, "1", "no!"); // returns `boolean | string`
console.log(typeof o[1]); // now `string`! oh no! |
Is this essentially the same problem as the following? interface DictWithSpecifics {
[key: string]: boolean | string;
hello: boolean;
}
let x: DictWithSpecifics = { hello: true };
x["he" + "llo"] = "bad"; |
Yeah, pretty much. I think the root of the hole is that index signatures are only checked covariantly today (as we only check variance of functions), but because props are read/write by default, technically they should be invariant with respect to the other index signatures and properties they need to be compatable with (excepting a read-only indexer, which already behaves correctly since it would need to be covariant). |
Yep, my understanding is that when coming across a computed property access, all the other properties are dropped and assignment is only considered for the indexer. If the type was expressed as an intersection like type DictWithSpecifics = { [key: string]: boolean | string; } & { hello: boolean; } then it's essentially eliminating the right branch. So I think the options might be to either make the computed property type satisfy the intersection of the indexers, or to make the indexers invariant as you suggest. EDIT: Previous comment was unintelligible as I brought up intersection out of nowhere. |
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed. |
TypeScript Version: HEAD
Search Terms: 23592, index number string keyof
Code
*** Inconsistent Behavior ***
BothStringsAndNumbersNotLegal
should be legal, it's just the union type that is returned bykeyof
. It should be sugar forBothStringsAndNumbersLegal
when the values have the same type. I find it strange that I can't set the index type to the exact type that is returned bykeyof
(without indirection).numbersAllowed
above only gives an error for some of the ways to set a string key onto the interface.Ok so I don't think the first two are that controversial. But I think the next one will be:
OnlyStringsKeys
should bestring
and notstring | number
OR the types for numbers and strings must be required to be the same.Yes, I know that is the legacy behavior that is enabled with the special compiler flag. It's a breaking change and that makes it not ideal in the first place. But, beyond that, it's inconsistent.
I presume the rationale is that integers are cast to strings and so the string type has to be a superset of the number type because the values will be merged into the same key space.
But consider this code:
It's possible for the merged namespace to cause the type of the string to infect the number type resulting in a value that is incorrectly typed.
I think there's reasonable arguments to be made for one behavior or the other, but I think something has to change.
Any argument that says that the type
OnlyStringsKeys
has to also include numbers because casting, needs to also say that the value type for number indexes and string indexes has to be the same -- because casting.If numbers are allowed to have their own type, then the argument must be that the risk of namespace collision for strings/numbers is sufficiently low (or a programmers responsibility to manage). By that argument then strings should also be allowed to have their own type that is not required to be a superset.
The new code for indexed properties as it stands right now is going to cause me to make a new type:
Which will replace all the uses of
keyof Obj
for my dictionary types. It's a lot of code changes. I can update this issue with the exact number later. I never use numbers for these types. I don't want to ever pass a number to interfaces unless I said so. But TS allows it:If I wanted numbers in my type, I would have said so with the type of the
key
for the indexed property (by giving two indexes, or a single one with a union type as I suggest above).If the type checker never allows me to put a number into an object with that interface, then I don't have to worry about namespace collisions. That means the type of
keyof T
can reasonably bestring
unless it's declared to allow numbers. This preserves backwards compatibility for keyof. But it breaks backwards compat for interfaces that are declared with a string index, but are setting numbers in them. In my opinion, this is better. Those interfaces should be updated to declare their index type as beingstring | number
, thekeyof
for those types would then match the intent of that interface. This is the kind of type narrowing backwards incompatibility that I'm used to when I upgrade TS.I think this change to the
keyof
types is really great in general, but in its current state on master I'm not a fan of some of these rough edges.Related Issues: #23592
The text was updated successfully, but these errors were encountered: