Instantly share code, notes, and snippets.

@bheklilr /creators.ts Secret
Last active Jan 2, 2019

Embed
What would you like to do?
import { creator, ActionsOf, Actionable } from "./redux-helpers";
// A creator is just an object with fields that are functions.
// The creator function makes sure that the object has the
// correct type, and feeds the value of the action key in as
// a type level constant, which was the main part of this trick.
// I usually have a do, complete, and failed for most actions.
export const example = {
do: () => creator("/App/example/do", {}),
failed: (reason: string) => creator("/App/example/failed": { reason }),
}
// This uses the ActionsOf and Actionable type helpers to infer
// the types of the actions. A little bit of tooling could make
// this generated too. You don't technically need the individual
// ones either, in most cases, as the makeReducer function infers
// them based on the action key.
export type ExampleActions = ActionsOf<typeof example>;
// The field names passed in as the second type argument are
// type checked and auto-completed by VS Code
export type ExampleDoAction = Actionable<typeof example, "do">;
export type ExampleFailedAction = Actionable<typeof example, "failed">;
// The types above are equivalent to
// type ExampleActions = Action<"/App/example/do"> | (Action<"/App/example/failed"> & { reason: string; })
// type ExampleDoAction = Action<"/App/example/do">
// type ExampleFailedAction = Action<"/App/example/failed"> & { reason: string }
import { makeReducer } from "./redux-helpers";
import { ExampleActions } from "./creators";
// VS Code will auto-complete the action keys for you here thanks
// to type inference. No need to make extra variables to keep up
// with them. You also can't put in an action key that isn't part
// of the Actions type provided to makeReducer, and each reducer
// just takes the store and the action, all type checked, and has
// to return a partial store.
// The other really nice thing is that the store and action parameters
// have their types inferred too, so the reducer is really quite simple
// to write and maintain.
export const reducer = makeReducer<ExampleActions>("Example", {
"/App/example/do": (store, action) => ({error: null}),
"/App/example/failed": (store, { reason }) => ({error: reason}),
});
import { ReducersMapObject, Action, Reducer, AnyAction, DeepPartial } from "redux";
/**
* A reducer that returns a partial state
*/
export type PartialReducer<S, A extends Action<string>> = (store: S, action: A) => DeepPartial<S>;
/**
* Given an action creator collection, infers the types of all of the contained action creators
*/
export type ActionsOf<T> = T[keyof T] extends (...args: any[]) => any
? ReturnType<T[keyof T]> extends Action<string> ? ReturnType<T[keyof T]> : never
: never;
/**
* Given an action creator collection and a field name in that collection, infers the type
* of the action associated with that field name
*/
export type Actionable<T, K extends keyof T = keyof T> = T[K] extends (...args: any[]) => any
? ReturnType<T[K]> extends Action<string> ? ReturnType<T[K]> : never
: never;
/**
* Extracts the action key type from an action
*/
export type ActionKey<T> = T extends Action<infer Key> ? Key : never;
/**
* Picks an action out of an ActionsOf<T> based on the ActionKey
*/
export type PickAction<T, Key extends ActionKey<T>> = T extends Action<Key> ? T : never;
/**
* Given a store and a set of actions, creates the type of all reducers
* for those actions. This gets Partial'd later on, but is nicer to see
* in its entirety here
*/
export type ReducersForActions<S, Actions extends Action<string>> = {
[K in ActionKey<Actions>]: PartialReducer<S, PickAction<Actions, K>>
};
/**
* The type of an action creator
*/
export type Creator<Key extends string, T> = (payload: T) => Action<Key> & T;
/**
* Creates an action given the key and the payload, the trick
* here is the Key parameter which gets inferred based on the
* string passed in
*/
export function creator<Key extends string, T extends object>(
key: Key,
payload: T
): Action<Key> & T {
return Object.assign({ type: key }, payload);
}
export class ReducerManager<Store> {
/**
* Must be parameterized based on a set of actions, and
* given a name since I keep up with all of my reducers by
* name. Don't register two reducers with the same name.
*/
public makeReducer = <Actions extends Action<string>>(
name: string,
reducers: Partial<ReducersForAction<Store, Actions>>
): Reducer<Store, Actions> => {
// implements the logic to register the reducers, it's
// not terribly complicated, but this is the point where
// I'm not sure how much I can share. Gotta leave some
// homework for the reader anyway =P
}
// This is called when setting up the Redux context
public getGlobalReducer = () => {}
public registerReducer = (name: string, reducer: Reducer<Store>) => {}
}
// I don't do it exactly like this, but the concept is pretty much the same.
// There's a single global reducer manager that gets instantiated for my
// store, then I export the makeReducer for that instance of the manager.
export const REDUCER_MANAGER = ReducerManager<MyStore>(initialState);
export const makeReducer = REDUCER_MANAGER.makeReducer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment