2

I was trying to add function arguments overloads starting from the most specific but type narrowing doesn't seem to work. Also tried changing arguments to a union type, but type guards don't work either. What am I missing?

type IReducer<S, A> = (state: S, action: A) => S;

interface IAsyncHandlers<S, A extends IAction> {
  request?: IReducer<S, A>;
  success?: IReducer<S, A>;
  failure?: IReducer<S, A & { payload: any; error: true }>;
}

interface IAction {
  type: string;
  payload: any;
}

const getActionHandler = <S, A>(handler?: IReducer<S, A>) => (state: S) => (action: A): S =>
  handler ? handler(state, action) : state;

const handleAsyncAction = <S, A extends IAction>(handlers: IAsyncHandlers<S, A>): IReducer<S, A> => {
  function reducer(state: S, action: A): S 
  function reducer(state: S, action: A & { error: true }): S;
  function reducer(state: S, action: A & { meta: { isPending: true } }): S;

  function reducer(state: S, action: A & { error?: any; meta?: any } ): S {  
    switch (true) {
      case action.error:
        // Property 'error' is optional in type 'IAction & { error?: any; meta?: any; }' 
        // but required in type '{ payload: any; error: true; }'.
        return getActionHandler(handlers.failure)(state)(action);

      case action.meta && action.meta.isPending:
        return getActionHandler(handlers.request)(state)(action);

      default:
        return getActionHandler(handlers.success)(state)(action);
    }
  }

  return reducer;
};

1 Answer 1

1

There are a number of things going on here.

First of all, you really don't need overloads: the only difference in those signatures is the type of action. Overloads are useful when the multiple signatures differ in some coordinated way; for example, if the type of the return value of the function depends on the type of action, or if the state parameter type depends on the type of action. Since nothing else in the signatures depends on the type of action, you can get the same behavior from the caller's side by (as you tried) changing the action to a union type (which is essentially just A, since A | (A & B) | (A & C) | (A & D) is essentially equivalent to A.)

As an aside, your overloads are actually ordered from least to most specific, which is backwards from what you generally want. The call signatures are examined in order from top to bottom. If a call does not match the first signature reducer(state: S, action: A): S, it will definitely not match any of the subsequent signatures reducer(state: S, action: A & XYZ): S. That means only the first signature will ever get used in practice. If overloads were needed here, I'd tell you to put the more specific ones first, and provide more details about what makes something "specific". But this doesn't really matter because you don't need overloads.

Your problem is really inside the implementation of the function, where you try to use a switch statement as a type guard on the type of the action variable. Unfortunately, the type of action involves A, a generic type parameter, and TypeScript does not do narrowing on generic parameters. It has been requested (microsoft/TypeScript#24085), but apparently such narrowing would cause significant performance problems with the compiler. My suggestion to you is to use user-defined type guards to exert more control over the narrowing that happens. It's a bit more verbose but it should work:

const isErrorAction =
  <A extends IAction & { error?: any }>(a: A): a is A & { error: true } =>
    (a.error)

const isRequestAction =
  <A extends IAction & { meta?: { isPending?: any } }>(
    a: A
  ): a is A & { meta: { isPending: true } } =>
    (a.meta && a.meta.isPending);

const handleAsyncAction = <S, A extends IAction>(
  handlers: IAsyncHandlers<S, A>
): IReducer<S, A> => {
  function reducer(state: S, action: A): S {

    if (isErrorAction(action)) {
      return getActionHandler(handlers.failure)(state)(action);
    }

    // not strictly necessary to narrow here, but why not
    if (isRequestAction(action)) {
      return getActionHandler(handlers.request)(state)(action);
    }

    return getActionHandler(handlers.success)(state)(action);
  }

  return reducer;
};

Now the implementation type-checks without errors, and the call signatures have been simplified to a single one.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.