DEV Community

Discussion on: Pragmatic types: Redux as Finite State Machine

qm3ster profile image
Mihail Malo

Turns out there's some trickery at work, but I never have to think about it because it's in another file in my actual projects.

type FullEvent<EP, T extends keyof EP> = { type: T } & EP[T]

type FullEvents<EP> = { [T in keyof EP]: FullEvent<EP, T> }

type AnyEvent<EP> = FullEvents<EP>[keyof EP]

// Your code begins here

interface EventPayloads {
  increment: { increment: number }
  reset: {}
}

type Event = AnyEvent<EventPayloads>

type State = number

export const matchReducer = (state: State, event: Event): State => {
  switch (event.type) {
    case "increment":
      return state + event.increment
    case "reset":
      return 0
  }
  // Uncommenting this disables exhaustiveness checking
  // throw new Error("Unsupported Event")
}

type Reducer<S, EP, T extends keyof EP> = (
  state: S,
  event: FullEvent<EP, T>,
) => S

const reducersLookup: {
  [T in Event["type"]]: Reducer<State, EventPayloads, T>
} = {
  increment: (s, e) => s + e.increment,
  reset: () => 0,
}

export const lookupReducer = (state: State, event: Event): State => {
  const reducer = reducersLookup[event.type] as Reducer<
    State,
    EventPayloads,
    typeof event.type
  >
  if (!reducer) {
    // this is fine, because our exhaustiveness check already happened when constructing the lookup
    throw new Error("Unsupported Event")
  }
  return reducer(state, event)
}

Also, in my case all variadic properties are under event.data, not root of event, so event always looks like {type: T, data: EventPayloads[T]}, but I wanted to implement your case.

Thread Thread
stereobooster profile image
stereobooster Author

It appears it is pretty easy in TS

type ActionTypes = 'increment' | 'reset'
const reducers: { [key in ActionTypes]: () => void } = {
    increment: () => {},
    reset: () => {}
}
Thread Thread
stereobooster profile image
stereobooster Author

And with Flow too

type ActionTypes = 'increment' | 'reset' 
const reducers = {
    increment: () => {},
    reset: () => {}
}
const reducer = (a: ActionTypes) => {
  const r = reducers[a];
}
Thread Thread
qm3ster profile image
Mihail Malo

Making them take the correct Action object is the more hacky part.
But I guess it makes sense, switch takes place inside function body, which does more inference, while the lookup is statically declared functions, which always requires more annotation.