Reputation: 6476
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
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!
Upvotes: 1