Here is a piece of code i got:
interface Obj {
a: number
b: string
}
const obj: Obj = {
a: 1,
b: 'hi'
}
function fn(key: keyof Obj, value: Obj[keyof Obj]) {
let foo = obj[key]
obj[key] = value
}
fn("a", 2)
So what i want to do is, i want the function fn()
to be able to update the object obj
properties, the first argument of this function is any key that obj
has(defined in Obj
interface), and the second argument is the value you wanna give to that key.
However, the typescript popup with an error in the obj[key] = value
this line, which is:
Type 'string | number' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'.(2322)
Here is a screenshot:
And a strange thing happens here, if you hover to the variable foo
(which is line 13 in the picture), it says:
let foo: string | number
which means, obj[key]
's type is string | number
, but the error says obj[key]
type is never
.
so my first question is: How come a
string | number
type magically becomes a never type? Is there any way to fix this?
Then i got another piece of code which solves this problem:
interface Obj {
a: number
b: string
}
const obj: Obj = {
a: 1,
b: 'hi'
}
function fn<K extends keyof Obj>(key: K, value: Obj[K]) {
obj[key] = value
}
fn("a", 2)
Therefore my second question would be: why using
Generics
solve the problem and what the hack is the keywordextends
here?
BTW, all the code are tested in typescript 3.7.5
version.
I am not a native English speaker, hope i explained my confusion clearly.
What about the error, it's because keyof Obj
might be "a"
or "b"
which have type number
or string
. In the expression obj[key]
the compiler doesn't know the property type, it might be number
or string
as well, so it disallows such assignment. Here is the same question. And you can find the explanation here, see Fixes to unsound writes to indexed access types.
In case of the generic function K extends keyof Obj
means that K
type can be "a"
or "b"
as well, but when you call the function fn("a", 2)
you implicitly set K
to "a"
, the compiler infers K
type from the first argument. So now, inside the call context key
has "a"
type and Obj[K]
is number
, hence the assignment becomes correct.
I just tried to explain the difference to my wife, who isn't a programmer :) I think it might be helpfull too:
Usual function: Lets imagine you are eating a cake but your eyes are closed. You know that it might be a cherry cake or a banana cake. You like the taste but you cannot say "What a delicious banana cake!" because you are not sure that it's a banana cake.
Generic function: In this case you eyes are open and you can choose the cake you want to eat, but you still have two choices: cherry or banana. Now, if you've chosen the banana cake and tasted it, you can say "What a delicious banana cake!".
Thanks for you explanation, however i just got one stupid question, as you said: "you implicitly set K to a", but why can't
keyof Obj
do the same? why can't implicitly setkeyof Obj
to 'a' as well? What really happens under the hood?@Limboer Generics allow you to specify a type when you call a function. A usual function argument types are constant, like
keyof Obj
in your case. In case of the generic function you tell the compiler that the function accepts an argument of typeK
and the type should satisfy the constraintextends keyof Obj
, in other words it must be"a"
or"b"
. When you call the function you specify the type explicitly likefn<"a">("a", 2)
or it can be implicitly inferred from the arguments you pass to the function, but now it's not the union type"a" | "b"
but just"a"
. I hope it'll help you@Limboer I've also added a real life explanation to the answer, I hope it might be helpful too