Reputation: 1
I am using React Redux toolkit and keyof to specify that one element of my action payload shall be the key of the type, that my state consists of, so I can update the properties of the state using a redux action. Anyways it says that: Type string | number is not assignable to type never. In this line:
state[id][key] = value;
Can you give me an explanation what the problem is here? Thank you very much!
interface MyType {
a: number;
b: string;
c: number;
};
const makeMyType = () => {
return {
a: 1,
b: 'b',
c: 2
} as MyType;
}
interface UpdateType<Type> {
id: number;
key: keyof Type;
value: Type[keyof Type];
}
const test_slice = createSlice({
name: 'test_slice',
initialState: [makeMyType(), makeMyType()];
reducers: {
updateProperty(state: MyType[], action: PayloadAction<UpdateType<MyType>) {
const {id, key, value} = action.payload;
state[id][key] = value;
}
}
});
Upvotes: 0
Views: 973
Reputation: 33091
This is the classic issue with mutations in typescript. You can find full and detailed explanation of this in my blog and in other SO answers:[ first, second, third ]
TL; DR
Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.
AND
objects are contravariant in their key types
Hence, state[id][key]
produces this error:
Type 'string | number' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'
This is because string & number = never
. See the first quote:.... contra-variant positions causes an intersection
.
TypeScript is unsure about state[id][key] = value
.
This type:
interface UpdateType<Type> {
id: number;
key: keyof Type;
value: Type[keyof Type];
}
is weak an allows illegal state to be represented. Consider next example:
const x: UpdateType<MyType> = {
id: 2,
key: 'a',
value: 's' //<--- should be number
}
If you want to make it safer, you should use union of all allowed/legal states:
type Values<T> = T[keyof T]
/**
* Is a union of all valid states
*/
type UpdateType<Type> = Values<{
[Key in keyof Type]: {
id: number;
key: Key;
value: Type[Key];
}
}>
But it did not help us to resolve the problem.
If you want to fix it , you should go one level up and mutate only state[id]
. This value has MyType
type.
There is one iimportant thing we should be aware of - TS does not track mutations. How we can benefit from this?
Consider this example:
const mutableUpdate = <
State extends MyType,
Key extends keyof State,
Value extends State[Key]
>(state: State, key: Key, value: Value) => {
state[key] = value;
return state
}
Above function will help us to mutate the state. Full example:
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface MyType {
a: number;
b: string;
c: number;
};
const makeMyType = (): MyType => ({
a: 1,
b: 'b',
c: 2
})
type Values<T> = T[keyof T]
/**
* Is a union of all valid states
*/
type UpdateType<Type> = Values<{
[Key in keyof Type]: {
id: number;
key: Key;
value: Type[Key];
}
}>
const mutableUpdate = <
State extends MyType,
Key extends keyof State,
Value extends State[Key]
>(state: State, key: Key, value: Value) => {
state[key] = value;
return state
}
const test_slice = createSlice({
name: 'test_slice',
initialState: [makeMyType(), makeMyType()],
reducers: {
updateProperty(state: MyType[], action: PayloadAction<UpdateType<MyType>>) {
const { id, key, value } = action.payload;
const result = mutableUpdate(state[id], key, value);
state[id] = result;
}
}
});
Upvotes: 1