Skip to content Skip to sidebar Skip to footer

How To Write A Typed Action Without Parameters

A Github issue suggests that we can use TypedAction.defineWithoutPayloadfor this purpose but I couldn't find any relevant examples on how to do so. I use this for login when an acc

Solution 1:

The way you have your typedAction function defined works fine:

exportfunction typedAction<T extendsstring>(type: T): { type: T };
exportfunction typedAction<T extendsstring, P extendsany>(
  type: T,
  payload: P
): { type: T; payload: P };
exportfunctiontypedAction(type: string, payload?: any) {
  return { type, payload };
}

The problem you're having is because of the destructuring of the action in your reducer parameters:

exportconst tokenReducer = (
  state: IState["token"] = null,
  { type, payload }: AppAction
): typeof state => {
  // ...
};

One of the difficulties with destructuring and TypeScript is that once you do that, the typing of the variables become independent from each other. Destructuring the action into { payload, type } makes a type: 'LOGIN' | 'LOGOUT' and payload: string | undefined variable. Even if you later refine the value of type, like in your switch statement, payload still has the type string | undefined; TypeScript will not automatically refine the type of payload in the case block after type is refined; their typings are completely independent.

So a somewhat ugly hack you can use is to not destructure:

export const tokenReducer = (
  state: IState['token'] = null,
  action: AppAction,
): typeof state => {
  switch (action.type) {
    case'LOGIN':
      return action.payload;
    case'LOGOUT':
      returnnull;
    default:
      return state;
  }
};

This works because in your switch statement, it's able to refine the action: AppAction type into the more specific login or logout types, so action.payload now is strongly tied to the payload type specific to one of those actions.

Here's an alternative pattern for redux actions I use that you might find more convenient at my fork that lets you enjoy the power of mapped types to define reducers with less boilerplate. First, you must define a type with the type/payload mappings and define some types deriving from that:

exporttypeActionPayloads = {
  LOGIN: string;
  LOGOUT: void;
};

exporttypeActionType = keyof ActionPayloads;

exporttypeAction<T extendsActionType> = {
  type: T;
  payload: ActionPayloads[T];
};

Your action creators can now be defined in terms of that map:

exportfunction typedAction<T extends ActionType>(
  type: T,
  payload: ActionPayloads[T]
) {
  return { type, payload };
}

Next, you can define a helper function for creating a strongly-typed reducer:

typeReducerMethods<State> = {
  [K inActionType]?: (state: State, payload: ActionPayloads[K]) =>State
};

typeReducer<State> = (state: State, action: AppAction) =>State;

function reducer<State>(
  initialState: State,
  methods: ReducerMethods<State>
): Reducer<State> {
  return(state: State = initialState, action: AppAction) => {
    consthandler: any = methods[action.type];
    return handler ? handler(state, action.payload) : state;
  };
}

(I haven't found a good workaround for that ugly : any cast, but at least we know logically that the typing is sound from the outside).

Now you can define your reducers thusly with nice implicit typing for your action handlers:

typeTokenState = string | null;

exportconst tokenReducer = reducer<TokenState>(null, {
  LOGIN: (state, token) => token, // `token` is implicitly typed as `string`LOGOUT: () =>null// TS knows that the payload is `undefined`
});

Post a Comment for "How To Write A Typed Action Without Parameters"