Skip to content

Instantly share code, notes, and snippets.

@AJamesPhillips
Last active January 10, 2020 17:28
Show Gist options
  • Save AJamesPhillips/4b36d407d2824d8ba114a96122fd312f to your computer and use it in GitHub Desktop.
Save AJamesPhillips/4b36d407d2824d8ba114a96122fd312f to your computer and use it in GitHub Desktop.
Type safe nested redux reducers
// state/reducers/schema.ts or spread through sub reducers/schema.ts files.
interface SettingsState {
favouriteColor: string
}
interface UsersState {
count: number
settings: SettingsState
}
interface MessagesState {
list: ({ fromId: number, toId: number, content: string })[]
}
interface AppState {
users: UsersState
messages: MessagesState
}
// state/reducers/utils.ts
type ObjectOfSelectors<State> = {[index: string]: (state: State, args?: any) => any }
function mapSelectors<Selectors extends ObjectOfSelectors<SubState>, State, SubState> (selectors: Selectors, stateGetter: (state: State) => SubState) {
const keys: (keyof Selectors)[] = Object.keys(selectors)
const mappedSelectors = keys.reduce((innerMappedSelectors, selectorName) => {
const value = selectors[selectorName]
innerMappedSelectors[selectorName] = ((state: State, args: any) => value(stateGetter(state), args)) as any
return innerMappedSelectors
}, {} as {[selectorName in keyof Selectors]: (
Parameters<Selectors[selectorName]>[1] extends undefined
? (state: State) => ReturnType<Selectors[selectorName]>
: (state: State, args: Parameters<Selectors[selectorName]>[1]) => ReturnType<Selectors[selectorName]>
)})
return mappedSelectors
}
// Fixed from: https://stackoverflow.com/a/59685402/539490
type UniqueObject<T, U> = { [K in keyof U]: K extends keyof T ? never : U[K] }
function safeMerge <T, U, V, W, X, Y, Z> (
a: T,
b?: UniqueObject<T, U>,
c?: UniqueObject<T & U, V>,
d?: UniqueObject<T & U & V, W>,
e?: UniqueObject<T & U & V & W, X>,
f?: UniqueObject<T & U & V & W & X, Y>,
g?: UniqueObject<T & U & V & W & X & Y, Z>,
) {
return {
...a,
...b,
...c,
...d,
...e,
...f,
...g,
}
}
// state/reducers/users/settings/index.ts
const settingsSelectors = {
getFavouriteColor: (state: SettingsState) => state.favouriteColor,
// isPositive: (state: SettingsState) => true, // errors correctly on merge below
}
// state/reducers/users/index.ts
const userSelectors = safeMerge(
mapSelectors(settingsSelectors, (state: UsersState) => state.settings),
{
numberOfUsers: (state: UsersState) => state.count,
isPositive: (state: UsersState) => state.count > 0,
}
)
// state/reducers/messages/index.ts
const messagesSelectors = {
getMessagesByFromUserId: (state: MessagesState, { userId }: { userId: number } ) => state.list.filter(m => m.fromId === userId),
getMessages: (state: MessagesState) => state.list
}
// state/reducers/index.ts
const mappedSelectors = safeMerge(
mapSelectors(userSelectors, (s: AppState) => s.users),
mapSelectors(messagesSelectors, (s: AppState) => s.messages),
)
const topSelectors = safeMerge(
mappedSelectors,
{
averageMessagesPerUser: (state: AppState) => mappedSelectors.getMessages(state).length / mappedSelectors.numberOfUsers(state),
// getMessages: (state: AppState) => state.messages.list, // correctly errors
}
)
// Test
const state: AppState = {
users: {
count: 4,
settings: { favouriteColor: "red" },
},
messages: {
list: [
{ fromId: 1, toId: 3, content: "Hi" },
],
}
}
function assert (a: any, b: any) {
if (a === b) {
console.log(`${a} === ${b}`)
}
console.assert(a === b, `${a} !== ${b}`)
}
assert(topSelectors.averageMessagesPerUser(state), 0.25)
assert(topSelectors.getFavouriteColor(state), "red")
assert(topSelectors.getMessages(state), state.messages.list)
assert(topSelectors.getMessagesByFromUserId(state, { userId: 1 })[0].content, state.messages.list[0].content)
assert(topSelectors.isPositive(state), true)
assert(topSelectors.numberOfUsers(state), 4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment