You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
3.5.1 (regression): "string cannot be used to index T" where T is Record<string, any> #31661
This is the intended behavior but I need to write several pages of documentation for "How do index signatures in generic constraints work [if at all]"
Reacted by Omeid Matten, Toni Vaakanainen, Mikhail Dudin, Eugene Ponomarov, artikesel and mantou
Reacted by Jack Williams, Damian Pieczynski, Russell Davis, Linus Unnebäck, Artyom Palvelev, Benyam Ephrem, guidelli, Yongwei, Tim Schmidt, Tarmo Aidantausta and 3 more
In general, the constraint Record<string, XXX> doesn't actually ensure that an argument has a string index signature, it merely ensures that the properties of the argument are assignable to type XXX. So, in the example above you could effectively pass any object and the function could write to any property without any checks.
In 3.5 we enforce that you can only write to an index signature element when we know that the object in question has an index signature. So, you need a small change to the clone function:
With this change we now know that objectClone has a string index signature.
Reacted by Baptiste Devessier, Pavel Luzanov, Julia Bubnova, Christopher Carman, Towry Wang, James Bate, Ryan Cavanaugh, Icey Von, jandvik, kkor and 3 more
I'm still a little confused by that explanation. Given this dubious bit of code, in both --strict and non-strict mode, the only complaint is the baggish["z"] = 3 line. I'm not sure why that line in particular gets to be the fatal error, and the only fatal error?
In general, the constraint Record<string, XXX> doesn't actually ensure that an argument has a string index signature, it merely ensures that the properties of the argument are assignable to type XXX.
I don't accept this "explanation".
Why is there even a place for the key type in Record<keyType, valueType if it has no meaning???
So, in the example above you could effectively pass any object and the function could write to any property without any checks.
That is the whole point!!! That's why the type is Record, string,...>! Note that "string" as type for the key and not some limited union.
Reacted by Omeid Matten, IronSpecs and Eugene Ponomarov
Also: Why would it EVER complain — in Javascript! — if I use a string as index in an object? By using Record I already told TS that it is meant to be a generic map object. The explanation does not make sense.
In general, coming from (the more strict!) Flow, all those many errors I now have in TypeScript when I iterate over my objects (using Object.keys) that "there is no index signature" are really, really... strange.
Alternatively, please document how to document generic (key-prop/value) objects when what I do is iterate over the keys. Without "any". Thus far I read the documentation to mean that Record<string, any> was meant to do exactly that?
Reacted by Andrej Kirejeŭ, danielamaia, Pierre Cavin, Fodor Zoltan, Ersin Akinci, Maximilian Kallert, Alexey Nosov, Damien Shaw, Kostiantyn Ko, Nathan Chappell and 13 more
Patrick Roza (@patroza) If that helps, and maybe in general anyway (I have not thought about wider implications), what if Object.keys and Reflect.ownKeys were declared that way (TS standard lib), instead of as Array<string | number | symbol> ?
As an experiment I changed the definition in lib.es2015.refelect.d.ts
and everything is PERFECT now! I had to remove the .filter(isString) because that widens the type to string again, but that filter is not needed in this context.
So maybe the underlying TS-lib definition can be looked and any maybe changed?
KittenWithHerbs (@lll000111) yea I was also surprised that the standard library didn't implement it this way at least for Object.keys, but I suppose it may have backwards incompatibility implications.
Just throwing my two cents in: In general I expect T extends SomeType (where SomeType is structural) to mean that T has at least the properties of SomeType, as that is what structural subtyping is all about, typically.
So my first instinct is that T extends Record<...> should likewise mean that T always has an index signature. But then I realize that this implies an index signature stands in for “all possible properties” which can’t be the case as this would be a bald faced lie: it’s physically impossible for an object to have “all possible properties”. So the best we can do is to say “it can have all possible properties”, in which case the current constraint behavior makes perfect sense.
I'm trying to understand this change, but having trouble with the current explanations. In particular, it's unclear why the change should apply specifically to a generic type T extends Bag and not to a regular non-generic Bag. For example, the "previously allowed crazy stuff" (from #31661 (comment)) is still allowed if you change function foo<T extends Record<string, any>>(obj: T) to function foo(obj: Record<string, any>). It's equally crazy in that context, no?
Anders Hejlsberg (@ahejlsberg), you tried to address the distinction at #31661 (comment), but it seems like that explanation has a similar problem. Your example shows that the new behavior prevents put2 from adding an extra (unknown) property and returning it as the original type (a Point). However, the following code compiles without any errors:
function put3(point: Point): Point {
point["z"] = 3;
return point;
}
const p3: Point = put3({ x: 3 });
So, again, why should the generic code be restricted more than the non-generic equivalent? I've been trying to unpack the following for a clue:
In effect, constraining a type parameter to Bag merely checks that T is assignable to Bag, but it doesn't imply that T is a Bag.
But again it's not clear how this is different from non-generic parameters -- if I pass a value to a non-generic parameter of type Bag, isn't it also just an assignability check? Isn't ~everything just an assignability check in a structural type system?
I think it will help a lot to see some specific examples of real-world errors this is intended to fix (if you only have time for a quick response, this would be the most helpful). So far, the compelling error cases I've seen (the "previously allowed crazy stuff" mentioned above) seem to be a result of the special behavior of Record<string, any> being assignable to any type.
Are there many errors you're seeing that don't involve that? If not, I would put forth an alternate proposal: for generic constraints, only ignore index signatures equivalent to Record<string, any>, and go back to the old behavior for all others.
I would put forth an alternate proposal: for generic constraints, only ignore index signatures equivalent to Record<string, any>
Thinking about this a bit more, this still doesn't feel great in terms of consistency, though at least it would be a more targeted special case. A better, even more targeted change would be to not ignore any index signatures, but to ignore the "Record<string, any> is assignable to any type" rule when it appears as a generic constraint.
That would eliminate the "previously allowed crazy stuff" while preserving expected/consistent behavior everywhere else (including the code from the top of this issue). I'm curious how much existing code it would break (though it may also uncover new errors that even #30769 doesn't catch).
If anyone can point me to any context/history/motivation for the "Record<string, any> is assignable to any type" rule, that would also be helpful.
So having thought about this some more, particularly in light of #31102, it turns out the aforementioned "crazy stuff" is still allowed, and for the same reason even. We've only closed the loophole for generics, but this is still fine:
typeX={[key: string]: number};typeY={pig: number,cow: number,ape: number};lety: Y={pig: 812,cow: 1208,ape: 128};letx: X=y;x.whale=9001;// yeah, there's no way this is fitting on the elevator.console.log(y);
As long as the above continues to work, I suspect we will never see the end of issues like #31808... ☹️
edit:
It turns out I didn't read very closely and Russell Davis (@russelldavis) above me already made the exact same point. Oops.
But again it's not clear how this is different from non-generic parameters -- if I pass a value to a non-generic parameter of type Bag, isn't it also just an assignability check? Isn't ~everything just an assignability check in a structural type system?
I kind of agree with this, but then again I kind of don't. The difference is that in the generic case, you have a type variable T whose type you don't actually know. You can't do an actual structural comparison against it--all you know that it's a subset of its extends constraint (for whatever "subset" means structurally); there is no lower bound so this could in fact be very, very specific (it could even be never!). That seems subtly different from the non-generic case, where you do know the exact type, and the unsoundness is merely a consequence of not enforcing contravariance. In other words, the assignment of y to x should be an error only because the source object is mutable, whereas even in that case it still would be a valid type to use for T. At least, that's my thinking, anyway.
That being said, it does still look like inconsistency from an end-user point of view, and that's probably all that matters.
Bruce Pascoe (@fatcerberus) thanks for the input. I'm not 100% sold on the reasoning without seeing concrete examples, but either way I agree with your final point. If there are practical reasons for the change, we should focus on those.
This has been already commented but still adding it here for completeness and to help devs struggling with a simpler case.
In case you're creating a setter with some logic (Object.keys(fieldsData) as (keyof T)[]) guarantees that all the values in the array generated by Object.keys are keys of T.
Activity
felix9 commentedon May 29, 2019
Here's a similar case I ran into:
Compiling that produces this:
RyanCavanaugh commentedon May 29, 2019
This is the intended behavior but I need to write several pages of documentation for "How do index signatures in generic constraints work [if at all]"
felix9 commentedon May 29, 2019
Confusingly, this will compile without error (though
--noImplicitAnywill complain):ahejlsberg commentedon May 29, 2019
This is an effect of #30769 and a known breaking change. We previously allowed crazy stuff like this with no errors:
In general, the constraint
Record<string, XXX>doesn't actually ensure that an argument has a string index signature, it merely ensures that the properties of the argument are assignable to typeXXX. So, in the example above you could effectively pass any object and the function could write to any property without any checks.In 3.5 we enforce that you can only write to an index signature element when we know that the object in question has an index signature. So, you need a small change to the
clonefunction:With this change we now know that
objectClonehas a string index signature.felix9 commentedon May 29, 2019
I'm still a little confused by that explanation. Given this dubious bit of code, in both
--strictand non-strict mode, the only complaint is thebaggish["z"] = 3line. I'm not sure why that line in particular gets to be the fatal error, and the only fatal error?[Cosmos] Downcast to ItemDefinition
[Cosmos] Downcast to ItemDefinition (#3283)
lll000111 commentedon May 30, 2019
I don't accept this "explanation".
Why is there even a place for the key type in
Record<keyType, valueTypeif it has no meaning???That is the whole point!!! That's why the type is
Record, string,...>! Note that "string" as type for the key and not some limited union.lll000111 commentedon May 30, 2019
Also: Why would it EVER complain — in Javascript! — if I use a string as index in an object? By using
RecordI already told TS that it is meant to be a generic map object. The explanation does not make sense.In general, coming from (the more strict!) Flow, all those many errors I now have in TypeScript when I iterate over my objects (using Object.keys) that "there is no index signature" are really, really... strange.
Alternatively, please document how to document generic (key-prop/value) objects when what I do is iterate over the keys. Without "any". Thus far I read the documentation to mean that
Record<string, any>was meant to do exactly that?patroza commentedon May 30, 2019
Not sure if this helps the case:
const typedKeysOf = <T>(obj: T) => Object.keys(obj) as Array<keyof T>lll000111 commentedon May 30, 2019
Patrick Roza (@patroza) If that helps, and maybe in general anyway (I have not thought about wider implications), what if
Object.keysandReflect.ownKeyswere declared that way (TS standard lib), instead of asArray<string | number | symbol>?As an experiment I changed the definition in lib.es2015.refelect.d.ts
from (
PropertyKeyisstring | number | symbol)to
and everything is PERFECT now! I had to remove the
.filter(isString)because that widens the type tostringagain, but that filter is not needed in this context.So maybe the underlying TS-lib definition can be looked and any maybe changed?
patroza commentedon May 30, 2019
KittenWithHerbs (@lll000111) yea I was also surprised that the standard library didn't implement it this way at least for
Object.keys, but I suppose it may have backwards incompatibility implications.ahejlsberg commentedon May 30, 2019
Patrick Roza (@patroza) For the reason
Object.keysisn't typed as(keyof T)[], see #12253 (comment).4 remaining items
fatcerberus commentedon Jun 2, 2019
Just throwing my two cents in: In general I expect
T extends SomeType(whereSomeTypeis structural) to mean thatThas at least the properties ofSomeType, as that is what structural subtyping is all about, typically.So my first instinct is that
T extends Record<...>should likewise mean that T always has an index signature. But then I realize that this implies an index signature stands in for “all possible properties” which can’t be the case as this would be a bald faced lie: it’s physically impossible for an object to have “all possible properties”. So the best we can do is to say “it can have all possible properties”, in which case the current constraint behavior makes perfect sense.russelldavis commentedon Jun 6, 2019
I'm trying to understand this change, but having trouble with the current explanations. In particular, it's unclear why the change should apply specifically to a generic type
T extends Bagand not to a regular non-genericBag. For example, the "previously allowed crazy stuff" (from #31661 (comment)) is still allowed if you changefunction foo<T extends Record<string, any>>(obj: T)tofunction foo(obj: Record<string, any>). It's equally crazy in that context, no?Anders Hejlsberg (@ahejlsberg), you tried to address the distinction at #31661 (comment), but it seems like that explanation has a similar problem. Your example shows that the new behavior prevents
put2from adding an extra (unknown) property and returning it as the original type (aPoint). However, the following code compiles without any errors:So, again, why should the generic code be restricted more than the non-generic equivalent? I've been trying to unpack the following for a clue:
But again it's not clear how this is different from non-generic parameters -- if I pass a value to a non-generic parameter of type
Bag, isn't it also just an assignability check? Isn't ~everything just an assignability check in a structural type system?I think it will help a lot to see some specific examples of real-world errors this is intended to fix (if you only have time for a quick response, this would be the most helpful). So far, the compelling error cases I've seen (the "previously allowed crazy stuff" mentioned above) seem to be a result of the special behavior of
Record<string, any>being assignable to any type.Are there many errors you're seeing that don't involve that? If not, I would put forth an alternate proposal: for generic constraints, only ignore index signatures equivalent to
Record<string, any>, and go back to the old behavior for all others.This would hopefully make the language simpler and more consistent, and save Ryan Cavanaugh (@RyanCavanaugh) from having to write several pages of documentation 😄 (would love to hear your take on this, Ryan, and/or Anders).
russelldavis commentedon Jun 6, 2019
Thinking about this a bit more, this still doesn't feel great in terms of consistency, though at least it would be a more targeted special case. A better, even more targeted change would be to not ignore any index signatures, but to ignore the "
Record<string, any>is assignable to any type" rule when it appears as a generic constraint.That would eliminate the "previously allowed crazy stuff" while preserving expected/consistent behavior everywhere else (including the code from the top of this issue). I'm curious how much existing code it would break (though it may also uncover new errors that even #30769 doesn't catch).
If anyone can point me to any context/history/motivation for the "
Record<string, any>is assignable to any type" rule, that would also be helpful.fatcerberus commentedon Jun 7, 2019
So having thought about this some more, particularly in light of #31102, it turns out the aforementioned "crazy stuff" is still allowed, and for the same reason even. We've only closed the loophole for generics, but this is still fine:
As long as the above continues to work, I suspect we will never see the end of issues like #31808...☹️
edit:
It turns out I didn't read very closely and Russell Davis (@russelldavis) above me already made the exact same point. Oops.
fatcerberus commentedon Jun 7, 2019
I kind of agree with this, but then again I kind of don't. The difference is that in the generic case, you have a type variable
Twhose type you don't actually know. You can't do an actual structural comparison against it--all you know that it's a subset of itsextendsconstraint (for whatever "subset" means structurally); there is no lower bound so this could in fact be very, very specific (it could even benever!). That seems subtly different from the non-generic case, where you do know the exact type, and the unsoundness is merely a consequence of not enforcing contravariance. In other words, the assignment ofytoxshould be an error only because the source object is mutable, whereas even in that case it still would be a valid type to use forT. At least, that's my thinking, anyway.That being said, it does still look like inconsistency from an end-user point of view, and that's probably all that matters.
russelldavis commentedon Jun 7, 2019
Bruce Pascoe (@fatcerberus) thanks for the input. I'm not 100% sold on the reasoning without seeing concrete examples, but either way I agree with your final point. If there are practical reasons for the change, we should focus on those.
seancheung commentedon Jun 11, 2019
What about this?
haohello commentedon Oct 21, 2019
Any update on the deepMerge type implementation? I'm also stuck on this.
haohello commentedon Oct 21, 2019
How about this one:
Amourspirit commentedon Sep 18, 2020
Here is what I came up with. TypeScript Playground
MichaelCereda commentedon Jan 23, 2021
This has been already commented but still adding it here for completeness and to help devs struggling with a simpler case.
In case you're creating a setter with some logic
(Object.keys(fieldsData) as (keyof T)[])guarantees that all the values in the array generated byObject.keysare keys of T.Here's an example
aprilmintacpineda commentedon Jan 15, 2022
I ran into this:
managed fixed it through comments above: