6thfdwp / learning-thoughts

Record and Reflect
2 stars 0 forks source link

Type it in Redux with TypeScript utility #6

Open 6thfdwp opened 2 years ago

6thfdwp commented 2 years ago

Recently I read articles about better static type support in Redux using TypeScript utility, which can dynamically create new types based on declared types. These are pretty useful way to improve DX, mitigate the pain of Redux boilerplate and excessive type definitions.

From implementation ➟ type

The main part is how to type the action object used in reducers, based on the what's returned from action creators. Let's consider basket scenarios which normally have actions to manipulate product items in it.

const BasketActions = {
  addItem: (item: Product, quantity: number) => {
    return {
      type: "BASKET:ADD_ITEM",
      payload: { item, quantity },
     // to demo how each action can be properly inferred in reducer 'case'
      extraProp: {key: 1}
    } as const;
  },
  removeItem: (productId: number) => {
    return {
      type: "BASKET:REMOVE_ITEM",
      payload: { productId }
    } as const;
  }
};

Is there a way to type the 'returned' object without manually defining them? As we've already had the action object in each action creator function. Essentially use implementation to infer its returned type as discussed here

type AddItem = {type: 'BASKET:ADD_ITEM', payload: {item: Product, quantity: number}} 
type RemoveItem = {type: 'BASKET:REMOVE_ITEM', payload: {productId: string}} 

ReturnType comes in handy

export type valueof<T> = T[keyof T];
// generics take action creator (e.g type of BasketActions) as parameter
export type ActionType<TActions extends { [k: string]: any }> = ReturnType<
  valueof<TActions>
>;

// type def 
type BaskeActionType = ActionType<typeof BasketActions>
export type AppActionTypes = BasketActionType | OtherActionType | ...

Let's break it down to see how the action object is inferred:

  1. typeof BasketActions The typeof operator returns
    {'addItem': (item: Product, quantity: number) => {..}, 'removeItem': (productId: number) => {...} }
  2. valueof<TActions> This essentially extracts each function from step 1 type:
    (item: Product, quantity) => {type: 'BASKET:ADD_ITEM', payload: {item: product}, extraProps: {key: number}} | (productId) => {...}
  3. ReturnType<step2 type> This finally returns object shape from each function type of step 2

AppActionTypes is a new type which is union of all action objects from each action creator's functions.

Happy auth-completion in Reducer

We can see it's more efficient and reliable to write in reducer now:
All the action.type string const can be auto filled when writing each case. Also the action object is properly inferred in each case. They don't even need to have the same shape

image

The full 'Basket' example can be found here

6thfdwp commented 2 years ago

Refs

Typed Actions with less keystrokes Create types from other types React TypeScript Cheatsheet