Angel S. Moreno
Angel S. Moreno

Reputation: 3961

How to fix Type 'string' is not assignable to type 'T[keyof T]'

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

Answers (1)

jcalz
jcalz

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.

Playground link to code

Upvotes: 22

Related Questions