supasate / connected-react-router

A Redux binding for React Router v4
MIT License
4.73k stars 593 forks source link

how to properly type the reducer #195

Open AdrienLemaire opened 5 years ago

AdrienLemaire commented 5 years ago

Related to https://github.com/supasate/connected-react-router/issues/173

versions:

I'm trying to declare my root state but I keep getting errors on History which I haven't been able to resolve after hours.

reducer.ts

const persistConfig: PersistConfig = {
  key: "root",
  storage: localStorage,
};

export type RootState = ApiState &
  FirebaseState & {
    router: Reducer<RouterState, LocationChangeAction>;
  };

// connected-react-router v5 requires exporting a function accepting history
const createRootReducer: Reducer<RootState> = (history?: History) =>
  persistReducer(
    persistConfig,
    combineReducers({
      api,
      auth,
      firebase: persistReducer(
        {
          key: "firepersist",
          storage: localStorage,
          stateReconciler: hardSet,
        },
        firebaseReducer,
      ),
      router: connectRouter(history),
      ui,
      ...otherReducers,
    }),
  );

export default createRootReducer;

type check

$ tsc --project tsconfig.json
src/reducers/index.ts:52:7 - error TS2322: Type '(history?: History<any> | undefined) => Reducer<{ application: any; contact: any; faq: any; mediaList: any; releases: any; api: {}; auth: { isLoaded: boolean; isEmpty: boolean; userId: any; }; firebase: any; router: {}; ui: any; } & PersistPartial, AnyAction>' is not assignable to type 'Reducer<RootState, AnyAction>'.
  Types of parameters 'history' and 'state' are incompatible.
    Type 'RootState | undefined' is not assignable to type 'History<any> | undefined'.
      Type 'RootState' is not assignable to type 'History<any> | undefined'.
        Type 'RootState' is missing the following properties from type 'History<any>': length, action, location, push, and 7 more.

52 const createRootReducer: Reducer<RootState> = (history?: History) =>
         ~~~~~~~~~~~~~~~~~

src/reducers/index.ts:66:29 - error TS2345: Argument of type 'History<any> | undefined' is not assignable to parameter of type 'History<any>'.
  Type 'undefined' is not assignable to type 'History<any>'.

66       router: connectRouter(history),
                               ~~~~~~~

Found 2 errors.

error Command failed with exit code 1.

If I cannot solve this issue, I'll change Reducer<RootState> → Reducer and persistReducer(…) → persistReducer(…) as any, but this wouldn't be sane becore my exported RootState wouldn't guarantee that the expected state is correct.

I understand that history is undefined at the beginning. Do you know of a way to say "that's ok" this way ?

damacisaac commented 5 years ago

Couple things that might help:

arnaud-zg commented 5 years ago

I'm not sure if it's the best solution, but it seems like combineReducers only want a function that returns a Reducer (given by redux). It works if I explicitly cast the result as Reducer like that:

router: connectRouter(history) as Reducer,
kahurangitama commented 5 years ago

I'm not sure if it's the best solution, but it seems like combineReducers only want a function that returns a Reducer (given by redux). It works if I explicitly cast the result as Reducer like that:

router: connectRouter(history) as Reducer,

Type 'Function' is not assignable to type 'Reducer<{}, AnyAction>'

kahurangitama commented 5 years ago
src/client/store/reducers.ts:17:3 - error TS2322: Type '<S>(reducer: Reducer<S, AnyAction>) => Reducer<S, AnyAction>' is not assignable to type 'Reducer<Reducer<{}, AnyAction>, AnyAction>'.
  Types of parameters 'reducer' and 'state' are incompatible.
    Type 'Reducer<{}, AnyAction> | undefined' is not assignable to type 'Reducer<{}, AnyAction>'.
      Type 'undefined' is not assignable to type 'Reducer<{}, AnyAction>'.

17   router: connectRouter(history),
     ~~~~~~

  src/client/store/reducers.ts:17:3
    17   router: connectRouter(history),
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    The expected type comes from property 'router' which is declared here on type 'ReducersMapObject<{ constructor: {}; toString: string; toLocaleString: string; valueOf: Object; hasOwnProperty: boolean; isPrototypeOf: boolean; propertyIsEnumerable: boolean; account: { isFetching: boolean; ... 9 more ...; addresses: null; } | { ...; } | { ...; } | { ...; }; ... 8 more ...; router: Reducer<...>; },...'

Please assist!

UPD: ended up with temporary solution by adding:

// @ts-ignore
router: connectRouter(history),
favna commented 5 years ago

The best way I ended up finding is to use the typesafe-actions library to property type both your actions and your reducer @kahurangitama.

You can find an example here: https://github.com/Favna/ReactTSDemo/blob/master/src/store

(note: also navigate to ProjectRoot/typings/typesafe-actions/index.d.ts which gets overwritten with the proper RootActions as per the lib's requirement for typesafe reducer)

Edit: since the sample code above lacks connected-react-router use this as stand in for the index.ts file from the linked folder:

import {
  connectRouter, go, goBack, goForward,
  push, replace, RouterState
} from 'connected-react-router';
// other imports

const routerActions = {
  push: typeof push,
  replace: typeof replace,
  go: typeof go,
  goBack: typeof goBack,
  goForward: typeof goForward,
};

export type ApplicationState = Readonly<{
  router: RouterState;
  app: DemoState;
}>;

export function* rootSaga () {
  yield all([fork(demoSaga)]);
}

export const rootActions = {
  router: routerActions,
  counter: counterActions,
};

export type RootAction = ActionType<typeof rootActions>;

export default (history: History) => combineReducers<ApplicationState>({
  router: connectRouter(history),
  app: DemoReducer,
});