Created
April 19, 2019 18:24
-
-
Save karlrwjohnson/892e1d97c00ebc3a8fd3a4439a7c4d53 to your computer and use it in GitHub Desktop.
Type-safe reducer builder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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