Last active
January 10, 2020 17:28
-
-
Save AJamesPhillips/4b36d407d2824d8ba114a96122fd312f to your computer and use it in GitHub Desktop.
Type safe nested redux reducers
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
// 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