chr
chr

Reputation: 1

keyof and React Redux action

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

Answers (1)

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;
    }
  }
});

Playground

Upvotes: 1

Related Questions