Dynamic method signature based on prop?

I have a function that will update the field of an object like so:

const obj = {a: 1, b:2};
const changeObj = (field, value) => obj[field] = value;

This is obviously simplified, but I'm trying to make this typed properly based on the field property that is passed in. This is as close as I've gotten:

interface ITest {
  name: string;
  anotherProp: number;
  lastProp: IOtherInterface;
}

const obj: ITest = {name: 'test', anotherProp: 1, lastProp: {a: 'b'}};
const changeObj = (field: keyof ITest, val: ITest[keyof ITest]) => obj[field] = val;

Which gets me most of the way there, but technically val can have a value type of string, number, or IOtherInterface. Is there a way to tell the function that if I'm passing in name and val can only be a string in that case?


Solution 1:

In order for this to work you want changeObj to be a generic function:

const changeObj = <K extends keyof ITest>(
    field: K, val: ITest[K]
) => obj[field] = val; // okay

The field parameter is of type K, a type parameter that's constrained to keyof ITest. When you call changeObj(), the compiler will infer K based on what's passed into field. Then the val parameter will be checked against ITest[K], the type of the value you get if you index into ITest with a key of type K:

changeObj("name", "hey") // okay
changeObj("name", 123) // error, number not a string

This makes it much less likely that you'll pass the wrong thing into changeObj. But it doesn't completely prevent it; if field is of a union type, then the compiler will also accept a val of the analogous union type, which could cause problems:

changeObj(Math.random() < 0.99 ? "name" : "anotherProp", 123); // okay?
// uh oh, very good chance of a problem

This sort of unsoundness is just part of TypeScript; the trouble situation doesn't crop up frequently enough to be addressed in the language itself.

Playground link to code