Description
Do you want to request a feature or report a bug?
Feature
What is the current behavior?
Right now, if I have a state defined by
interface IState {
navBarSelection: NavBarSelection,
potato: number,
}
export enum NavBarSelection {
Home,
About
}
and define my reducers like this:
class ActionTypes {
public static SET_SELECTED_NAV = 'SET_SELECTED_NAV';
}
const navBarReducer = (state: NavBarSelection = NavBarSelection.Home, action: any): NavBarSelection => {
switch(action.type) {
case ActionTypes.SET_SELECTED_NAV:
return action.payload.selectedNav;
}
return state;
}
let statePropertyToReducerMap = {
navBarSelection: navBarReducer,
extraProperty: () => {},
};
let combinedReducers = combineReducers<IState>(statePropertyToReducerMap);
Typescript will fail to catch two potential errors:
- The statePropertyToReducerMap contains "extraProperty", which does not exist on the state.
- The statePropertyToReducerMap does not contain "potato", which does exist on the state.
Note the definitions that currently exist regarding combineReducers:
export interface ReducersMapObject {
[key: string]: Reducer<any>;
}
and
export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
What is the proposed behavior?
If we modify the definition of ReducersMapObject to
export type ReducersMapObject<S> = {
[P in keyof S]: Reducer<S[P]>;
};
Then this automatically catches our errors and we get the following two errors:
First, we get
Type '{ navBarSelection: (state: NavBarSelection, action: any) => NavBarSelection; extraProperty: () => void; }' is not assignable to type 'StatePropertyNameAndTypeAwareReducer'.
Object literal may only specify known properties, and 'extraProperty' does not exist in type 'StatePropertyNameAndTypeAwareReducer'.
and when we fix that by removing extraProperty, we get
Type '{ navBarSelection: (state: NavBarSelection, action: any) => NavBarSelection; }' is not assignable to type 'StatePropertyNameAndTypeAwareReducer'.
Property 'potato' is missing in type '{ navBarSelection: (state: NavBarSelection, action: any) => NavBarSelection; }'.
which is everything we've wanted.
Note that this change is a breaking change, since we are converting ReducersMapObject from an interface to just a type. I am not sure whether we can get the same effect with an interface, though if we want to keep the current definition of ReducersMapObject we could just add the type right underneath it like
export type StateAwareReducersMapObject<S> = {
[P in keyof S]: Reducer<S[P]>;
}
and then modify combineReducers to be
export function combineReducers<S>(reducers: StateAwareReducersMapObject<S>): Reducer<S>;
This way, people who relied on the old ReducersMapObject can keep using it, but the TypeScript compiler will start showing them errors when their reducers do not add up to create the state.
I haven't thought through all the edge cases (what if the object passed to combineReducers breaks up its responsibilities to delegated method calls that spread their result into the object), but it seems doing something along the lines of the proposal above would go a long way to making combineReducers more type safe. Sorry if I'm discussing a well-known approach or I'm overstepping my bounds here :)
At the very least we can maybe just add the StateAwareReducersMapObject type to the type definition for people to optionally use. Could still provide a lot of benefits. (note: keyof is only available in TypeScript >= 2.1)
What are your thoughts?
Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?
Latest Redux