2

Newish to Typescript. I and moving a project to TS and I am struggling to define the Typescript for my reducers when creating a slice using redux-toolkit. The scenario is I have filters stored in a state. I have a query string and an object of fields that will be filtered if a value is present. The type of each field depends on the data stored in that field. To set a field filter value I have created a reducer that accepts a key and value pair as the payload. I then use that to set the filter value for that field. After all of my Googling, ChatGPTing and Gemini-ing I am still getting the following error.

Type 'string | string[]' is not assignable to type 'string[] & string'.
  Type 'string' is not assignable to type 'string[] & string'.
    Type 'string' is not assignable to type 'string[]'.ts(2322)
(property) filters: WritableDraft<FilterValues>

This is the code for the slice. I have tried multiple types for the payload of setFilter and removeFilter and have left two of the setFilters to show what I have tried.

import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit';
import { RootState } from "redux/store";

export type FilterValues = {
  tags_map: string[];
  reference: string;
  time_executed: string;
  type: string;
}

interface State {
  filterQuery: string;
  filters: FilterValues;
}

const initialState:State = {
  filterQuery: '',
  filters: {
    tags_map: [],
    reference: '',
    time_executed: '',
    type: '',
  },
};

type KeyValuePayload<T> = {
  [K in keyof T]: { 
    key: K; 
    value: T[K]
  }
}[keyof T];

export const slice = createSlice({
  name: 'filters',
  initialState,
  reducers: {
    setFilterQuery: (state, { payload }) => {
      state.filterQuery = payload
    },
    // setFilter : (state, { payload }: PayloadAction<{ key: keyof FilterValues, value:  FilterValues[keyof FilterValues] }>) => {      
    //   state.filters[payload.key] = payload.value
    // },
    setFilter : (state, { payload }: PayloadAction<KeyValuePayload<FilterValues>>) => {     
      const { key, value } = payload
      state.filters[key] = value
    },
    removeFilter: (state, { payload }: PayloadAction<keyof FilterValues>) => {      
      state.filters[payload] = initialState.filters[payload]
    },
    removeAllFilters: (state) => {      
      return { ...state, filters: initialState.filters }
    },
  },
});

export const { 
  setFilterQuery,
  setFilter,
  removeFilter,
  removeAllFilters,
} = slice.actions;

export default slice. Reducer;

Sample payload

setFilter({key: "reference", value: "foobar"})
setFilter({key: "tags_map", value: ["foo", "bar"]})

I am at a loss. I used the approach explained in this answer, and trying it in TS playground it infers the correct types. But it I am still getting errors. I am sure there is a simple answer to this.

Thank you in advance.

2
  • Hey, I have a hard time figuring out what your Payload actually is supposed to like. Can you maybe give a simple example of that concrete type being passed to that action? Commented Feb 23, 2024 at 22:02
  • Opps! @Samulsen, certainly I have updated the question. Commented Feb 23, 2024 at 23:12

2 Answers 2

2

This does give you the "outer api shape" that's appropriate for calling the setFilter action creator for you, but inside your setFilter reducer function, that code is just far too generic to ever be correctly typed by TypeScript.

There's a limit where TypeScript can help you, and you're far beyond that limit - your code is just too generic. You'll have to slap a few anys in there and call it a day. as any means "I know more than the compiler", and in this case, that's all you can do.

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

3 Comments

Thank you @phry. I was confused as calling setFilter enforced the type correctly and thought that they type inside setFilter would be inferred from the payload type. Still wrapping my head around TS. Appreciate the answer. Should I be taking a different approach to seting the filter values?
Generally, I'd recommend to avoid setter-style reducers like this, (see the Redux Style Guide), but for something like a filter it's probably okay. The problem you are facing here is that the type you get as a payload can have so many different meanings that TS cannot keep track of it anymore correctly. So with code like that, you won't get around ignoring compiler errors.
Thank you 🙏. Really appreciate your help understanding this. Thanks for the info on style also. In this case the user action really is setting a filter, but I have user setter style reducers elsewhere and going to rethink them.
0

so essentially I see now the problem after you provided the sample Payload. Essentially phry is right, TypeScript can only go so far with its compiler, but I was able to hack around that limitation by using a TypeUnion approach combined with if statements that help the compiler understand what is going on. At least in my TypeScript environment that did the trick:

type FilterValues = {
  tags_map: string[];
  reference: string;
  time_executed: string;
  type: string;
};

type State = {
  filterQuery: string;
  filters: FilterValues;
};

type KeyValue =
  | { key: "tags_map"; value: string[] }
  | { key: "reference"; value: string }
  | { key: "time_executed"; value: string }
  | { key: "type"; value: string };


const initialState: State = {
  filterQuery: "",
  filters: {
    tags_map: [],
    reference: "",
    time_executed: "",
    type: "",
  },
};

const setFilter = (payload: KeyValue) => {
    const { key, value } = payload;
    if (typeof value === "string" && !(key === "tags_map")) {
        initialState.filters[key] = value
    }
    if (Array.isArray(value) && key === "tags_map") {
        initialState.filters[key] = value
    }
};

setFilter({ key: "tags_map", value: ["tag1", "tag2"] }); // no error
setFilter({ key: "tags_map", value: "ref1" }); // has error

Its a bit verbose, but you get basically what you want :)

1 Comment

Thank you @samulsen. You are right, putting type guards in does overcome the errors. I think given types are enforced when calling setFilter I am ok with as any approach as I know I can't send it the wrong payload, and its cleaner code. As I am adopting Typescript I just thought using any was the cardinal sin. I really appreciate your help in understanding this one.

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.