Skip to content

[typescript + createAsyncThunk()] What's the suggested approach for 'dynamic' typing? #1457

@mark-night

Description

@mark-night

This is not an issue but more of a question regarding understanding redux design philosophy (and so I did not ask on SF).

Say, I have a store slice for different animals:

interface AnimalState {
  dogs: Dog[];
  cats: Cat[];
  ...
}

const initialState: AnimalState = {
  dogs: [],
  cats: []
}

const animalsSlice = createSlice({
  name: 'animals',
  initialState,
  reducers: {/* ... */},
  extraReducers: builder => { /* ... */ }
})

A typical async thunk for creating new dog could be:

export const createDog = createAsyncThunk<
  Dog,
  Omit<Dog, 'id'>,
  { /* ... */ }
>('animals/createDog', async (dogData) => { /* thunk logic */ })

This works great. The thing is, the one for creating new cat (and all other animals) are exactly the same, so I want to make it a little bit more flexible to avoid repeat codes.

The first approach I tried, was to play with the type generic for createAsyncThunk(), where I tried to fit in an union to infer the animal to be created, something like this:

type Kind = 'dog' | 'cat';
type Animal<T extends Kind> = T extends 'dog' ? Dog : Cat;

export const createAnimal = createAsyncThunk<
  Animal<K>, // <-- is it possible to define `K` somewhere so the generic can pick it up?
  { kind: K; data: Omit<Animal<K>, 'id'> }, // <-- so animal kind could be inferred from thunkArg...
  { /* ... */ }
>('animals/create', async (animalData) => { /* ... */ })

This obviously won't work, because I can't define K anywhere for the generic... but you get the idea.

The second approach I tried, is to create an async thunk factory, something like:

function getAsyncThunk<K extends Kind>(kind: K) {
  return createAsyncThunk<
    Animal<K>,
    Omit<Animal<K>, 'id'>,
    { /* ... */ }
  >(`animals/create${kind}`, (animalData) => { /* ... */ })
}

export const createDog = getAsyncThunk('dog')
export const createCat = getAsyncThunk('cat')
...

The second approach works fine, but still, feels kinda repetitive, and will add more work on the reducer part. I would definitely prefer more on createAnimal('dog') rather than createDog(), createCat()......

Question:

  • What is the suggested approach/pattern for this kind of usage?
  • Is the idea behind the first approach (abstracting common action creators) an anti-pattern? I.e. one action creator touching multiple slice fields or even multiple state slices should be avoided?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions