Search code examples
reactjstypescriptgenericsreduxredux-toolkit

Redux toolkit createAction with generic type


I'm trying to use Redux toolkit createAction to generate an action that take a generic type. Types are as follows.

export interface ObjectArray {
    externalId: string
}

export interface CarFilters {
  arrayType: string[];
  objectType: ObjectArray[];
}

export type FilterOptionValue = string | number;

export type FilterMapValue = FilterOptionValue[] | string;

export interface ShipFilters {
  [key: number]: FilterMapValue;
}

type AppliedFiltersType = {
    cars: CarFilters;
    ships: ShipFilters;
}

type PathValue<T extends object, K extends keyof T = keyof T> = T[K] extends unknown ? T[K] : never;

export type AssingValueType<
  Category extends keyof AppliedFiltersType = keyof AppliedFiltersType, 
  Key extends keyof PathValue<AppliedFiltersType, Category> = keyof PathValue<AppliedFiltersType, Category>
> = {
  category: Category,
  filterKey: Key
  value: PathValue<AppliedFiltersType[Category], Key>;
}

Reducer and actions

import { createAction, createReducer } from "@reduxjs/toolkit";

import { AssingValueType, AppState } from "./types";

export const updateAppliedFilterValue = createAction<AssingValueType>(
  "UPDATE_APPLIED_FILTER_VALUE"
);

const initialState = {
  appliedFilters: {
    cars: {
      arrayType: [],
      objectType: []
    },
    ships: {}
  }
} as AppState;

const reducerCreator = createReducer(initialState, (builder) => {
  builder.addCase(updateAppliedFilterValue, (state, action) => {
    state.appliedFilters[action.payload.category] = action.payload.value;
  });
});

export default reducerCreator;

Intention is to make the action parameters strict type checked so it cannot be called with inconsistent types.

Eg

updateAppliedFilterValue({ category: 'car', filterKey: 1, value: 3 })

should be identified as a error from ts compiler.

Pure function like follows works

const updateFilterValues = <Category extends keyof AppliedFiltersType, Key extends keyof PathValue<AppliedFiltersType, Category>>(params: AssingValueType<Category, Key>) => {

  return params;
}

But I'm not clear on how to create generate action with createAction I have trouble creating action inferring the generic types.

Codesandbox Link


Solution

  • It seems that the issue you are having is strictly related to the definition of AssigningValueType.

    Solution:

    export type AssingValueType = {
      [Category in keyof AppliedFiltersType]:
        { category: Category } & {
          [Key in keyof AppliedFiltersType[Category]]: {
            filterKey: Key,
            value: PathValue<AppliedFiltersType[Category], Key>
          }
        }[keyof AppliedFiltersType[Category]]
    }[keyof AppliedFiltersType]
    

    Explainer AssingValueType is a Discriminated Union Type with Category a keyof AppliedFiltersType aka 'ships' | 'cars'.

    For each potential Category we also define filterKey in a similar fashion. value can be defined relative to Category and Key.

    & type intersection operator is used to merge the { filterKey, value } with { category }.

    The last and most important step is the {[Category in keyof AppliedFiltersType]: ...}[keyof AppliedFiltersType] which extracts a union type for each of the keys of AppliedFiltersType.

    In short... {ships: {category: 'ships', ...}, cars: {category: 'cars', ...}} becomes {category: 'cars' ...} | {category: 'ships'}

    Regarding your initial concern, createAction should work like a charm the way you intended to use it.