Skip to content

Instantly share code, notes, and snippets.

@karlrwjohnson
Created April 19, 2019 18:24
Show Gist options
  • Save karlrwjohnson/892e1d97c00ebc3a8fd3a4439a7c4d53 to your computer and use it in GitHub Desktop.
Save karlrwjohnson/892e1d97c00ebc3a8fd3a4439a7c4d53 to your computer and use it in GitHub Desktop.
Type-safe reducer builder
/*
createReducer() builds a reducer function using an initial state and a map of reducer functions.
Any action whose type matches a key in the reducer map will cause createReducer()'s return value to invoke a method in the reducer map.
If you annotate your dispatch() function as DispatchFor<yourReducerHere>, it will prevent you from passing an event that it's incapable of handling.
*/
import { Action } from 'redux';
// Define an action whose metadata is stored in its "payload" field.
interface PayloadAction<K, P> extends Action<K> {
payload: P
}
// Family of actions which correspond to keys on a reducer map ("RM")
type ReducerMapAction<S, RM, K extends keyof RM> =
RM[K] extends (state: S) => S ?
Action<K> :
RM[K] extends (state: S, payload: infer P) => S ?
PayloadAction<K, P> :
"UNABLE TO INFER ACTION TYPE - Did you declare a state on createReducer()'s defaultState?";
// Reducer type (to replace Redux's version) which exposes which kinds of actions it can accept
export interface Reducer<S, RM> {
<K extends keyof RM>(state: S, action: ReducerMapAction<S, RM, K>): S;
// Reducer is augmented with a property declaring its default state because that's what the combiner expects
defaultState: S
}
// External-facing macro for declaring that a dispatch function can operate on a given reducer.
export type DispatchFor<R extends Reducer<any, any>> =
R extends Reducer<infer S, infer RM> ?
<K extends keyof RM>(action: ReducerMapAction<S, RM, K>) => ReducerMapAction<S, RM, K> :
'ERROR INFERRING TYPE OF DISPATCH FUNCTION FROM REDUCER'
/**
* Build a reducer using a default state and an object mapping action types to handler functions.
*
* @param defaultState - The starting state. *Please explicitly declare its type!*
* @param reducers - Each method of this function will be invoked with the initial state
*
* @param S - Inferred from defaultState's type.
* @param RM - Inferred from the reducer's object. PLEASE DON'T make this inferred using
* Partial Type Argument Inference (assigning it to "= any" by default). It breaks
* Typescript's ability to infer the types of events that can be passed to DispatchFor<>.
*/
export function createReducer<S, RM>(defaultState: S, reducers: RM): Reducer<S, RM> {
const reducer = <K extends keyof RM>(state: S = defaultState, action: ReducerMapAction<S, RM, K>) => {
let nextState: S = state;
const { type, payload } = action as PayloadAction<K, any>;
if (type in reducers) {
// @ts-ignore
const reducerFunction = reducers[type] as (state: S, payload: any) => S;
const nextState = reducerFunction(state, payload);
if (!nextState) {
console.warn(`Reducer for ${type} failed to return usable value (${nextState}). Did you miss a return statement?`);
}
}
console.log(action, nextState);
return nextState;
};
reducer.defaultState = defaultState;
return reducer;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment