0

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

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.