OliverRadini
OliverRadini

Reputation: 6476

Ensure correspondence between key and value types

If I have a type:

interface ITest {
    a: number;
    b: string;
}

I may want to defined a function that sets a property on an object with that interface dynamically.

I can get some of the way, however the following code does not seem to properly compare the key and value types:

interface ITest {
    a: number;
    b: string;
}

const item: ITest = {
    a: 1,
    b: 'A Test',
}

const setValue = <K extends keyof ITest>(x: ITest) =>
    (key: K) =>
        (value: ITest[K]) => x[key] = value; 

Gets part of the way. However, from putting this through the playground, we get inconsisitent results:

interface ITest {
    a: number;
    b: string;
}

const item: ITest = {
    a: 1,
    b: 'A Test',
}

const setValue = <K extends keyof ITest>(x: ITest) =>
    (key: K) =>
        (value: ITest[K]) => x[key] = value; 

setValue(item)('a')('test');
setValue(item)('b')(false);    // only this ones shows an error
setValue(item)('b')(1);
setValue(item)('b')('b');      // if properly working, only this will work

This seems to imply that the compiler is happy provided value is a type of any of the values on ITest, which is not quite ideal.

Upvotes: 1

Views: 46

Answers (1)

jcalz
jcalz

Reputation: 330216

The definition should probably be:

const setValue = (x: ITest) =>
    <K extends keyof ITest>(key: K) =>
        (value: ITest[K]) => x[key] = value;

which you can test:

setValue(item)("a")(3); // okay
setValue(item)("b")("hello"); // okay
setValue(item)("c")(123); // error!
//  ---------> ~~~
// "c" is not assignable to "a" | "b".
setValue(item)("a")("whoops"); // error!
//  --------------> ~~~~~~~~
// "whoops" is not assignable to number

The idea is that setValue itself is a concrete, non-generic function, but the function that setValue(item) returns should be generic in the type K of key, and then the value parameter of the function that returns should be constrained to ITest[K].


Note that you don't want setValue itself to be generic in K, or else when you call setValue(item), the type of K will be specified already, and then the returned function will be a concrete one of type (key: keyof ITest)=>(value: ITest[keyof ITest])=>ITest[keyof ITest], which allows bad things like setValue(item)("a")("whoops"):

const badSetValue = <K extends keyof ITest>(x: ITest) =>
    (key: K) =>
        (value: ITest[K]) => x[key] = value;

badSetValue(item)("a")(3); // okay
badSetValue(item)("b")("hello"); // okay
badSetValue(item)("c")(123); // error!
//  ------------> ~~~
// "c" is not assignable to "a" | "b".
badSetValue(item)("a")("whoops"); // okay?!  uh oh

Also, since setValue() is so general in its implementation, you might decide you want it to be even more general:

const generalSetValue = <T>(x: T) =>
    <K extends keyof T>(key: K) =>
        (value: T[K]) => x[key] = value;

which works like setValue() on ITest objects,

generalSetValue(item)("a")(3); // okay
generalSetValue(item)("b")("hello"); // okay
generalSetValue(item)("c")(123); // error!
//  ----------------> ~~~
// "c" is not assignable to "a" | "b".
generalSetValue(item)("a")("whoops"); // error!
//  ---------------------> ~~~~~~~~
// "whoops" is not assignable to number

but works equally well on other object types also:

const otherThing = { name: "Harry Potter", age: 11 } 
generalSetValue(otherThing)("age")(17); // okay

Okay, hope that helps; good luck!

Link to code

Upvotes: 1

Related Questions