TypeScript: 3.5.1 (regression): "string cannot be used to index T" where T is Record
This is new as of 3.5.1, there was no error in 3.4.5.
Error message:
TS2536: Type 'string' cannot be used to index type 'T'
Obviously wrong, since T
is defined as Record<string, any>
.
function isString (thing: unknown): thing is string {
return typeof thing === 'string';
}
function clone<T extends Record<string, any>>(obj: T): T {
const objectClone = {} as T;
for (const prop of Reflect.ownKeys(obj).filter(isString)) {
objectClone[prop] = obj[prop];
}
return objectClone;
}
Playground Link — It will not show an error until the Playground uses the new TS 3.5.x
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 21
- Comments: 28 (8 by maintainers)
Commits related to this issue
- [Cosmos] Downcast to ItemDefinition - Required by improved soundness check introduced in TS 3.5.1 - https://github.com/microsoft/TypeScript/issues/31661 — committed to mikeharder/azure-sdk-for-js by mikeharder 5 years ago
- [Cosmos] Downcast to ItemDefinition (#3283) - Required by improved soundness check introduced in TS 3.5.1 - https://github.com/microsoft/TypeScript/issues/31661 — committed to Azure/azure-sdk-for-js by mikeharder 5 years ago
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?What about this?
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]”
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
clone
function:With this change we now know that
objectClone
has a string index signature.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-genericBag
. For example, the “previously allowed crazy stuff” (from https://github.com/microsoft/TypeScript/issues/31661#issuecomment-497138929) 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?@ahejlsberg, you tried to address the distinction at https://github.com/microsoft/TypeScript/issues/31661#issuecomment-497474815, 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 (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 @RyanCavanaugh from having to write several pages of documentation 😄 (would love to hear your take on this, Ryan, and/or Anders).
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 thebaggish["z"] = 3
line. I’m not sure why that line in particular gets to be the fatal error, and the only fatal error?Not sure if this helps the case:
const typedKeysOf = <T>(obj: T) => Object.keys(obj) as Array<keyof T>
@felix9 The distinction becomes evident when you consider both input (parameter) and output (function result) positions. Imagine that
put1
andput2
returned the object passed to them:Following a call to
put1
you now have aBag
andBag
is not assignable toPoint
. And because the argument has been promoted toBag
, the function is permitted to write to any property.However, following a call to
put2
you still have aPoint
. So, the type checker needs to restrict the function to only allow writing to properties that are known to exist inT
. In other words, it should only allow assignments toT[K]
whereK
is akeyof T
or something constrained tokeyof T
. In effect, constraining a type parameter toBag
merely checks thatT
is assignable toBag
, but it doesn’t imply thatT
is aBag
.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???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.That would one way to do it. However, it is very common to initialize map-like objects using object literals, and for an object literal we know the exact set of properties from which to compute an implicit index signature. So we permit the assignment for types originating in object literals.
I’d write it like this:
No need to use index signatures anywhere. And, to recap, for the reason
Reflect.ownKeys
is not typed as(keyof T)[]
, see https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208.Confusingly, this will compile without error (though
--noImplicitAny
will complain):How about this one:
(1)
Here is the part I find highly confusing:
class Foo extends Bag
, it meansFoo
is aBag
function put<Foo extends Bag>
, it DOES NOT mean thatFoo
is aBag
As I am reading the discussion above, it seems to me that
{x: 3}
should not be assignable toBag
type, because it does not have any indexer property to satisfy theBag
interface.If that was the case, the the example
put2({x: 3})
above would trigger a compilation error.(2) Now going back to
foo
example:IMO, the code above is valid in the sense that it’s ok to assign to
s
andn
properties, becauseRecord<string, any>
does allow that. What I don’t consider as correct is to callfoo
with suchobj
value that does not have indexer property mapping arbitrary key toany
value.I think a more correct definition of
foo
would say that the Record type has only keys ofT
, something along the following lines:Such code is triggering the following error for me:
Element implicitly has an 'any' type because type 'Record<keyof T, any>' has no index signature.ts(7017)
, which is a good sign that we are doing something wrong here. Personally, I would expect a different error -Property 's' does not exist on type 'T'
.Because
foo
wants to work withs
andn
properties, I think it should list them in the Record template arguments.That way the compiler can verify:
foo
that we are accessing only thoseobj
properties that exist onT
foo
providesobj
value that allowss
andn
properties(3) When I think about the
clone
function mentioned earlier in the discussion, I would rewrite it as follows:This seems to work with TypeScript v3.5, although I am surprised that
Reflect.ownKeys
is returningstring[]
instead of(keyof T)[]
.Thoughts?
@patroza For the reason
Object.keys
isn’t typed as(keyof T)[]
, see https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208.