Using discriminated union type with a react reducer

63 Views Asked by At

Struggling to understand how to use a discriminated union type with a reducer. Here is how my reducer is setup below. I want to be able to pass this into a provider component, but keep hitting type errors.

export const enum LoadingState {
  idle = "idle",
  loading = "loading",
  loaded = "loaded",
  error = "error",
}

export type SampleState =
  | {
      status: LoadingState.idle;
    }
  | {
      status: LoadingState.loading;
    }
  | {
      status: LoadingState.loaded;
      sampleDetails: SampleDetails;
    }
  | {
      status: LoadingState.error;
      error: any;
    };

export const INITIAL_STATE: SampleState = {
  status: LoadingState.idle,
};

export type Action =
  | {
      type: "SET_SAMPLE_LOADING_STATE";
      payload: { status: LoadingState };
    }
  | {
      type: "SET_SAMPLE";
      payload: { sampleDetails: SampleDetails };
    };

export const reducer = (state: SampleState, action: Action) => {
  switch (action.type) {
    case "SET_SAMPLE_LOADING_STATE":
      return {
        ...state,
        status: action.payload.status,
      };

    case "SET_SAMPLE":
      return {
        ...state,
        status: LoadingState.loaded,
        sampleDetails: action.payload.sampleDetails,
      };

    default:
      return state;
  }
};


// Trying to use it here in a provider
const [state, dispatch] = useReducer(
  reducer,
  INITIAL_STATE
);

But I keep getting:

   Argument of type 'SampleState' is not assignable to parameter of type 'never'.
  Type '{ status: LoadingState.idle; }' is not assignable to type 'never

Is there something wrong with my setup here? If I remove the other types from the union, it works as expected.

1

There are 1 best solutions below

0
JW. On BEST ANSWER

Since you're merging the previous state with the next one, if the previous state was "error", and you use the "SET_SAMPLE_LOADING_STATE" action to set it to "idle", you could end up with an invalid state like:

{
  status: LoadingState.idle,
  error: 'something'
}

I think you can fix this by explicitly making reducer return SampleState, and having it make more fine-grained checks for which state you're setting, e.g.:

if (action.payload.status === LoadingState.idle) {
  return {
    status: LoadingState.idle,
  }
} else if ...