Reputation: 3961
Given the following Typescript function:
const setter = <T = Record<string, string>>(obj: T, prop: keyof T, val: string): void => {
obj[prop] = val;
};
I get the following error from my IDE:
Type 'string' is not assignable to type 'T[keyof T]'
If T is a Record of string keys and string values and prop
is a key of T, then I originally assumed that I could assign a string to T[keyof T]
.
What is the proper use of generics to fix this error?
Upvotes: 15
Views: 14549
Reputation: 327964
The immediate problem with
const setter = <T = Record<string, string>>(
obj: T, prop: keyof T, val: string
): void => {
obj[prop] = val; // error! 'string' is not assignable to 'T[keyof T]'
};
is that the generic type parameter T
can be absolutely anything the caller of the function wants it to be. All you've done with T = Record<string, string>
is specified that T
would default to Record<string, string>
in the (unlikely) event that the compiler could not infer anything for T
. But T
will be inferred by whatever value is passed in as obj
, if not specified manually by the caller:
setter({ a: 1 }, "a", "oops"); // no compiler error
So this isn't what you wanted to say.
Perhaps you were trying to constrain T
to Record<string, string>
instead of defaulting to it. That is denoted with extends
, not =
:
const setter = <T extends Record<string, string>>(
obj: T, prop: keyof T, val: string
): void => {
obj[prop] = val; // Type 'string' is not assignable to type 'T[keyof T]'
};
That would at least prevent people from making completely incorrect calls where obj
has a non-string
property at the relevant key::
setter({ a: 1 }, "a", "oops"); // error!
// ----> ~ // number is not assignable to string
But the problem with constraints like this is that the caller could narrow properties to something more specific than string
, such as a union of string literal types:
type Foo = { b: "x" | "y" };
const foo: Foo = { b: "x" };
badSetter2(foo, "b", "oopsie"); // no compiler error
Here, foo
is of type Foo
, an object type whose b
property must either be "x"
or "y"
. And we've apparently assigned "oopsie"
to its b
property, which shouldn't be allowed.
This kind of narrowing might be unlikely for your expected use cases, but the compiler is concerned about it, so the error remains. You can't necessarily assign string
to obj[prop]
.
One way to fix this is to make sure that val
is of a type that can be assigned to the specific key at prop
. That involves adding a type parameter for the key:
const setter = <T extends Record<string, string>, K extends keyof T>(
obj: T, prop: K, val: T[K]
): void => {
obj[prop] = val; // okay
};
setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // compiler error
setter({ z: "hello" }, "z", "goodbye"); // okay
Now everything works as desired. With prop
as type K
, and val
as type T[K]
(the type you get when you index into an object of type T
with a key of type K
), then val
can be seen as assignable to obj[prop]
. And the invalid calls to setter()
from above are rightly rejected.
That's probably the "right" way to do this.
You can simplify it a bit (at the expense of a little correctness) by removing T
entirely. All you care about is that obj
has a string
valued key at the prop
property, then you can leave K
and rephrase like this:
const setter = <K extends PropertyKey>(
obj: Record<K, string>, prop: K, val: string
): void => {
obj[prop] = val; // okay
};
setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // okay?!
setter({ z: "hello" }, "z", "goodbye"); // okay
This again prevents crazy calls, but still allows erroneous calls like setting foo.b
to "oopsie"
.
And finally, you could simplify this even further; if you don't even care about the particular key type K
, you can just make this a non-generic function:
const setter = (
obj: Record<string, string>, prop: string, val: string
): void => {
obj[prop] = val;
};
setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // okay?!
setter({ z: "hello" }, "z", "goodbye"); // okay
This is not worse than the previous one in terms of false negatives, but might have more false positives, which I won't get into because this answer is too long already.
So there you go. You have a spectrum of solutions to choose from ranging from fully generic to fully non-generic, any of which may be more or less useful depending on your use cases.
Upvotes: 22