I'm feeling as a cheater because I used @aleksxor no-type-safe-way
link. Sorry for that, I just thought that it worth some explanation.
I believe this argument is pretty good:
This is not an error.
T = { hello : "bye" }
Now, your assignment,
map["hello"] = "hi there"
Is unsound
Consider the following example:
let index: { [key: string]: any } = {}
let immutable = {
a: 'a'
} as const
let record: Record<'a', 1> = { a: 1 }
index = immutable // ok
index = record // ok
const foo = (obj: { [key: string]: any }) => {
obj['sdf'] = 2
return obj
}
const result1 = foo(immutable) // safe, see return type
const result2 = foo(record) // safe , see return type
It works, because TS does not try to infer obj
from generic argument. We can use any string we want as index.
Let's go back to our problem, now, TS tries to infer type of object
const foo = <T extends { [key: string]: any }>(obj: T) => {
obj['sdf'] = 2 // error
}
{[key:string]:any}
is too wide.
Examples of unsoundness:
let index: { [key: string]: any } = {}
let immutable = {
a: 'a'
} as const
let record: Record<'a', 1> = { a: 1 }
index = immutable // ok
index = record // ok
const foo = <T extends { [key: string]: any }>(obj: T) => {
obj['sdf'] = 2
return obj
}
const result1 = foo(immutable) // unsound, see return type
const result2 = foo(record) // unsound , see return type
Hence, while you can get value by key, it is safe to disallow mutations by key.
Btw, TS does not play well with mutations, because it can't track them.
Here you have another one good example why it is better to avoid mutations
If TypeScript didn't know the key exists in T, result[key] should be an error regardless of reading or writing. I get why writing may be unsound when reading isn't, I think that first sentence is just a tangent.
If you read property - it can not affect T
object and you can return T
without any problems.
const foo = <T extends { [key: string]: any }>(obj: T) => {
const readOperation = obj['sdf']
// object still has T type
return obj
}
But if you mutate it:
const foo = <T extends { [key: string]: any }>(obj: T) => {
obj['sdf'] = 2
// this is not our good old `T` anymore,
return obj
}
TS can't figure out the return type of function.
Type signature is
const foo: <T extends {
[key: string]: any;
}>(obj: T) => T
But it is not T
anymore, since it mutated and because TS does not track mutations - it is unsafe to do.