1
\$\begingroup\$

I need to write a custom reducer utility. It should take a dictionary of reducers and a publish subject to return a state, with a dispatch method to dispatch actions and notify the publish subject.

It should be 100% type-checked with TypeScript, compatible with strict compiler options, and should only use type-inference when applicable.

Link in the TypeScript Playground to see the types.

Implementation

type Action<T = string, S extends {} = any> = S & {
    type: T
}

type Reducer<S = any, A extends Action = Action> = (state: S | undefined, action: A) => S

interface Reducers<S = any, A extends Action = Action> {
    [key: string]: Reducer<S, A>
}

type ReducerState<R> = R extends Reducer<infer S, any>
    ? S
    : never

type ReducersActions<R extends Reducers> = {
    [K in keyof R]: R[K] extends Reducer<any, infer A>
        ? A
        : never
}[keyof R]

type StoresState<R extends Reducers> = {
    [K in keyof R]: ReducerState<R[K]>
}
type Stores<R extends Reducers> = StoresState<R> & {
    dispatch(action: ReducersActions<R>): void
}

interface Subject<T> {
    next(object: T): void
}

function createReducer<T extends Reducers>(
    reducers: T,
    publish: Subject<Stores<T>>
): Stores<T> {
    const state: Partial<Stores<T>> = {}

    state.dispatch = function dispatch(action) {
        Object
            .keys(reducers)
            .forEach(key =>
                state[key] = reducers[key](state[key], action)
            )

        publish.next(state as Stores<T>)
    }
    state.dispatch({type: ''} as Action)

    return state as Stores<T>
}

Test code

namespace Actions {
    export type AddTodo = Action<'add-todo', {todo: string}>
    export type RemoveTodo = Action<'remove-todo', {index: number}>
    export type Todos =
        | AddTodo
        | RemoveTodo

    export type ChangeInput = Action<'change-input', {input: string}>

    export type All =
        | Todos
        | ChangeInput
}

function todos(todos: string[] = [], action: Actions.Todos) {
    switch(action.type) {
        case 'add-todo':
            return [...todos, action.todo]
        case 'remove-todo':
            return todos.filter((_, i) => i === action.index)
    }

    return todos
}

function input(input: string = '', action: Actions.ChangeInput) {
    switch(action.type) {
        case 'change-input':
            return action.input
    }

    return input
}
const state = createReducer({todos, input}, {
    next: ({todos, input}) => console.log('state', {todos, input})
})

state.dispatch({
    type: 'change-input',
    input: 'do something'
})

state.dispatch({
    type: 'add-todo',
    todo: state.input
})
  • Can it be simplified? (e.g. less use of generics, get rid of TypeScript 2.8+ requirement for infer, etc..)
  • Can I prevent reducers in the  createReducer function from having a dispatch property?
  • Do you have any other suggestions/improvements?
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.