skip to Main Content

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

Answers


  1. 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.

    Login or Signup to reply.
  2. 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 🙂

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search