Edwin
Edwin

Reputation: 400

How to create a strongly typed generic function for redux slices and their actions?

I am using @reduxjs/toolkit and want to create an easily extendible function that creates a slice with default reducers. The implementation that I have now works but is not strongly typed. How can I create a function so that the slice's actions type contains not only the default reducers but also the ones that are passed in? I have tried using inference types but could not get it to work.

Any guidance would be appreciated. Thanks.

Minimum example: in common.ts file (where logic can be shared between slices)

export interface StoreState<T> {
  data: T
  status: 'succeeded' | 'failed' | 'idle'
  error: string | null
}

// create a slice given a name and make it possible to extend reducers so they include more than just reset and updateStatus
export const createStoreSlice = <T>(props: {
  sliceName: string
  defaultState: T
  reducers?: SliceCaseReducers<StoreState<T>> // <-- want to infer this in slices/<sliceName>.ts
}) => {
  const { sliceName, reducers, defaultState } = props

  const initialState: StoreState<T> = {
    data: defaultState,
    status: 'idle',
    error: null,
  }

  return createSlice({
    name: sliceName,
    initialState,
    reducers: {
      ...reducers, // <--- want to somehow infer the type of this when exporting slice actions
      reset: (state) => {
        Object.assign(state, initialState)
      },
      updateStatus: (state, action) => {
        state.status = action.payload
      },
    },
  })
}

in slices/<sliceName>.ts (specific slices with extra logic)

export const genericSlice = createStoreSlice({
  sliceName: 'someSliceName',
  defaultState: { someField: 'some value' },
  reducers: {
    setSomeField: (state, action) => {
      const { payload } = action
      state.data.someField = payload
    },
  },
})

// these actions should be strongly typed from the createStoreSlice function parameters and contain the default reducers (eg. reset, updateStatus) and extra ones specific to the slice (eg. setSomeField)
export const { reset, updateStatus, setSomeField } = genericSlice.actions 

Upvotes: 8

Views: 1681

Answers (1)

Nickofthyme
Nickofthyme

Reputation: 4327

You were really close but there were three parts you were missing

  1. You must infer the type of reducers with a generic type
  2. You must use the generic reducers type (i.e. R) along with the provided ValidateSliceCaseReducers type to determine the resulting type for reducers.
  3. Make reducers type required. I was only able to make it work with required reducers but there may be a way around this but unlikely as the types come from @reduxjs/toolkit.

Note: I just pulled out the createStoreSlice props into the StoreSliceProps type to make it easier to read.

import { SliceCaseReducers, createSlice, ValidateSliceCaseReducers } from '@reduxjs/toolkit';

export interface StoreState<T> {
  data: T;
  status: 'succeeded' | 'failed' | 'idle';
  error: string | null;
}

interface StoreSliceProps<T, R extends SliceCaseReducers<StoreState<T>>> {
  sliceName: string;
  defaultState: T;
  reducers: ValidateSliceCaseReducers<StoreState<T>, R>;
}

export function createStoreSlice<T, R extends SliceCaseReducers<StoreState<T>>>(props: StoreSliceProps<T, R>) {
  const { sliceName, reducers, defaultState } = props;

  const initialState: StoreState<T> = {
    data: defaultState,
    status: 'idle',
    error: null,
  };

  return createSlice({
    name: sliceName,
    initialState,
    reducers: {
      ...reducers,
      reset: (state) => {
        Object.assign(state, initialState);
      },
      updateStatus: (state, action) => {
        state.status = action.payload;
      },
    },
  });
};

export const genericSlice = createStoreSlice({
  sliceName: 'someSliceName',
  defaultState: { someField: 'some value' },
  reducers: {
    setSomeField: (state, action) => {
      const { payload } = action;
      state.data.someField = payload;
    },
  },
});

export const { reset, updateStatus, setSomeField, fakeReducer } = genericSlice.actions; // only fakeReducer throws error as unknown as expected

Here is a working TS Playground

See this example from their docs that explains it a bit better.

Upvotes: 6

Related Questions