TypeScript: `Type 'string' cannot be used to index type 'T'` when indexing a generic function parameter
Bug Report
đ Search Terms
Type âstringâ cannot be used to index type âTâ.
đ Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about index signature
⯠Playground Link
Playground link with relevant code
đ» Code
function foo<T extends Record<string | symbol, any>>(target: T, p: string | symbol) {
target[p]; // This is okay
target[p] = ""; // This errors
}
đ Actual behavior
(parameter) target: T extends Record<string | symbol, any>
Type 'string' cannot be used to index type 'T'.(2536)
Type 'symbol' cannot be used to index type 'T'.(2536)
đ Expected behavior
Either:
- It should work, as the getter already works
- Or at least the error message should be changed as itâs misleading, indexing itself has no problem.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 17
- Comments: 28 (12 by maintainers)
@fatcerberus my question is really simple⊠How can I say that
T
is a plain old JavaScript object that can have any fields keyed by a string? If I canât useRecord<string, any>
to do that what should I use? Iâm tired of hitting this error.Reflect.set(obj, key, 1)
works. But Iâd also love to find a way to useobj[key]=1
directly@fatcerberus Even though I went through the examples and discussion above, Iâm not sure I understand exactly what is the root cause of this error. Check the following example:
but why canât
test
be used? it is a string, and the constraint of T isextends Foo
, where the interface forFoo
specifies that the index signature is a stringThe error canât be correct, since it can be used to index type T. Itâs at least misleading.
What human intuitively expects when writing
function<T extends Record<string, any>(arg: T)
isarg
to be of a dictionary type with the ability to additionally specify its exact shape. From what I understand after reading this thread - unfortunately TypeScript is broken and does not work that way. Generic type can extend a type but the result is unpredictable.@Tinadim
T
is a type parameter, which means that it can be anything thatâs assignable to its constraint (i.e.extends
is not necessarily a subtyping relationship). This is maybe a bit counterintuitive, but being constrained by a type with an index signature doesnât guarantee thatT
also has one:test(x)
is legal, butx['test'] = 1234;
is not. Therefore, the assignment insidetest
is also invalid. When you write a generic function, itâs not just the value that varies, but the type as well, so the compiler wants to ensure that what you write inside the function is valid for all possible typesT
can be.But:
Maybe this is just what we should expect by using
any
? đ€@gabritto Similarly, this hole exists:
Just a thought for anyone who finds themselves here. I ended up using a one liner taking into account what @MartinJohns said about âextendingâ.
be aware target is entirely replaced with a new object.
The type of the property
a
is"hello"
, not the value (well, the value is too). Itâs a property that can only accept the string literal"hello"
, but it can not accept the string""
. Itâs more common used with union types, e.g."a" | "b"
. Meaning it can only be the string"a"
, or the string"b"
, but not any other string.Here goes a sort of personal summary of the situation, based on my recent investigation of this issue. Let me first say that index signatures are a bit confusing and inconsistent with the behavior of regular property declarations, so all the questions in this issue are very valid.
Now, why is the error happening? The error happens because we have a rule that, when you are writing/assigning to an element access expression, you cannot use an index signature present in the constraint of a generic type. However, you can use that same index signature if you are reading from that same element access expression:
If we think about it, writing
target[p] = ""
is not safe. Just imagine if we instantiateT
withRecord<string, "hello">
, which is a more specific type thanRecord<string, string>
: the type oftarget[p]
would be"hello"
, but we would be assigning""
to it.Ok, so we have a justification for not using the index signature of the constraint
Record<string, string>
ofT
when writing, so all is good, right? No, because in similarly unsafe situations, we do allow using an index signature or a property declaration of a type parameterâs constraint when writing:So, from
foo3
above we see that weâre not very rigorous about not using an index signature on the left-hand side of an assignment. And fromfoo4
above we see that regular property declarations (i.e.{ a: string }
) can potentially cause the same type violation/unsafety that we used to justify banning index signatures when writing, but we allow them just fine!In conclusion, the rules are not very consistent, and I donât have a precise explanation of why it works the way it does, but it probably has to do with how convenient it is to allow the potentially unsafe pattern vs disallowing patterns that are likely to cause bugs.
@Stevenic
You can use
Record<string, any>
(in fact thatâs exactly what that means, althoughRecord<string, unknown>
would be safer). In the example in this issue, the type oftarget
isnâtRecord<string, any>
, itâs some specialization ofRecord<string, any>
. It could beRecord<string, any>
, but it also could beRecord<"only this", any>
. Clearly this shouldnât be okay:If you want to treat the argument as any record with string keys, you donât need a generic at all. You can just use that type:
Now, there are two things I still donât understand, and which look like bugs to me on the face of them, but I havenât thought about these long:
(playground)
A useful way to think about generics is that youâre writing a function that works for all possible types that
T
could be; if this isnât true, you get an error in the implementation. You get no error at the call site, though, because there is no obvious issue with the call tofoo()
-Test
is assignable toRecord<string, any>
and"foo"
is astring
. Obviouslytarget[p] = "hello"
is not a valid thing to do with aTest
(or any number of other things you can pass tofoo
), so you get the error inside the function. It is a bit odd thattarget[p];
there is not also an error, though.Technically the call of
foo2()
should be an error, and remains unsound even if you change theany
tounknown
, but thatâs a separate issue: TS treats object types covariantly. Itâs unsound for the same reason assigning astring[]
to a(string | number)[]
is unsound. The type system doesnât really model mutation well.I keep hitting this error over and over⊠Can I just say that if the workaround is to use
Object.assign()
then something is completely broken. I just donât get it⊠Why is it ok to read fromtarget[p]
but not assign to it?T
is a type that extends a Record that has a string for a key and any type of value. Ifp
is a string then I should be able to assign a value to that field. What am I missing???My current workaround is to say screw you to the compiler and
(target as any)[p] = '';
Guess what I have yet to encounter a runtime exception because thereâs nothing wrong with that assignmentâŠwhat i find confusing is that iâm using a basic type like
Foo
orRecord<string, number>
and it still clashes. i would expect it to error out if i used a constraint likeT extends {foo: number; bar: number}
, but thatâs not the case here. sure, somebody can put in a type like{foo: number; bar: number;}
and even though it doesnât have the keytest
it still is aFoo
, so why shouldnât the function in your example return an object{foo: 1, bar: 2, test: 1234}
?maybe, what i would like to have is to be able to say something like
T is Record<string, number>
instead ofT extends Record<string, number>
so i can create a function that takes a somewhat generic dictionary, or array, or whatever and returns the same somewhat generic type but with more values or key value pairs than i put in.So, whatâs the good practice to index the generic type T inside the function?