DEV Community

Discussion on: Pragmatic types: Redux as Finite State Machine

Collapse
 
qm3ster profile image
Mihail Malo

How do you feel about using object literals instead of match?

Collapse
 
stereobooster profile image
stereobooster

I'm ok with it (in general), but I not sure how to do exhaustive check with object literals. From machine code point of view, this is a different code, but this doesn't matter unless this is "critical path" code.

Collapse
 
qm3ster profile image
Mihail Malo

I've found that it's universally faster.
In TypeScript exhaustive checks work, not sure about flow.
The default case is when indexing into the object returns undefined.

I like it for performance reasons and also because it seems more functional to me, containing values (which can also be functions when needed) and not expressions.

Thread Thread
 
stereobooster profile image
stereobooster

In TypeScript exhaustive checks work, not sure about flow.

Do you have a code snippet?

I've found that it's universally faster.

Based on benchmark or personal impression?

Thread Thread
 
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

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

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.