Reputation: 13
I'm trying to create a type based on another one with the same keys and whose value's type is based on, but not equal to, the another type. It's similar to React
's setState
, where it accepts either a value or a function that get the current value and returns a new one.
After research, all issues related to this error seems related to a lack of type hinting. I already tried to specified the types for all next
, current
and newTheme[key]
variables. It didn't work.
type Mode = "light" | "dark";
interface Theme {
mode: Mode;
test: number;
}
type ThemeUpdate = {
[K in keyof Theme]: Theme[K] | ((current: Theme[K]) => Theme[K])
};
const reducer = (currentTheme: Theme, action: Partial<ThemeUpdate>): Theme => {
const newTheme: Partial<Theme> = {};
(Object.keys(action) as (keyof Theme)[]).forEach((key: keyof Theme) => {
const next = action[key];
const current = currentTheme[key];
newTheme[key] = typeof next === "function" ? next(current) : next;
// ^^^^^^^
// Argument of type 'number | "light" | "dark"' is not assignable to parameter of type 'never'. Type 'number' is not assignable to type 'never'
});
return { ...currentTheme, ...newTheme };
};
What I expect is that the next
function will resolve the argument type according to the current key. Instead, the argument get resolved to the type never
and I get the error message:
Argument of type 'number | "light" | "dark"' is not assignable to parameter of type 'never'. Type 'number' is not assignable to type 'never'`
The next
function should infers the current
argument as Mode
when key === "mode"
, and when key === "test"
it should be inferred as number
Upvotes: 1
Views: 4498
Reputation: 330456
Yeah, this is one of those pain points in TypeScript that I've been calling correlated types. The problem is that you have a set of values of union types which are not independent of each other. next
is correlated to current
in a way that depends on the value of key
. But TypeScript just sees next
as a union of values-or-functions-or-undefined (which is correct) and current
as a union of values (which is also correct) without realizing that it is impossible for next
to correspond to "mode"
and for current
to correspond to "test"
at the same time. The improved support for calling unions of functions only makes this problem more confusing, since the never
intersection doesn't give much clue as to what's going on.
There's no great solution here. The ways to deal with this I've found are either to walk the compiler manually through the different cases, as in:
(Object.keys(action) as (keyof Theme)[]).forEach(key => {
switch (key) {
case "mode": {
const next = action[key];
const current = currentTheme[key];
newTheme[key] = typeof next === "function" ? next(current) : next; // okay
break;
}
case "test": {
const next = action[key];
const current = currentTheme[key];
newTheme[key] = typeof next === "function" ? next(current) : next; // okay
break;
}
}
});
which is quite type safe but is tedious...
Or, else you will need to make a type assertion somewhere to convince the compiler that what you are doing is safe (which is you taking on the responsibility for the safety; the compiler is giving up). For this particular issue, I'd consider making the forEach()
callback a generic function in the key type K
and then using your assertion to tell the compiler that next
is dependent on K
in a way it understands:
(Object.keys(action) as (keyof Theme)[]).forEach(
// generic
<K extends keyof Theme>(key: K) => {
const next = action[key] as // assert here
| Theme[K]
| ((current: Theme[K]) => Theme[K])
| undefined;
const current = currentTheme[key];
newTheme[key] = typeof next === "function" ? next(current) : next; // okay
}
);
which is more convenient but not as safe.
I generally recommend assertions here (link to code) and hope that one day better support for correlated types in TypeScript is introduced.
Okay, hope that helps. Good luck!
Upvotes: 3