Skip to content
Advertisement

How to make an elegant state transition map in Typescript?

If you can come up with a better title, please edit!

What I have are two enums, kinda like this:

enum State {
    A = "A",
    B = "B",
    C = "C"
}

enum Event {
    X = "X",
    Y = "Y",
    Z = "Z"
}

And I want to declare a map which says “If current state is S and event is E then the new state is S1“.

Not every state-event combination will have a new state, sometimes the event does nothing.

So, in pseudo-javascript, I want to write something like:

const transitions = {
    State.A: {
        Event.X: State.B,
        Event.Y: State.C
    },
    State.B: {
        Event.X: State.A
        Event.Z: State.C
    }
}

Unfortunately, I cannot find a way to write this in an elegant fashion. Object literals cannot have non-string-literal keys (not even interpolated) and indexer types in Typescript cannot be union types and… I’m stuck. Anything that I can come up which works is a lot more verbose and ugly.

Advertisement

Answer

Your answer works nicely, but I noticed there is a small issue with it.

Because you used a generic string index in StateMap, this means that you lose all type safety about how you access StateMap.

For example:

const stateTransitions: StateMap = {
    [State.A]: {
        [Event.B]: State.C
    }
};

const x = stateTransitions.whatever // x is { [index: string]: State }

We should be able to know that whatever can never exist on a StateMap.

Here’s one option:

type StateMap = Record<State, Record<Event, State>>

The downside of the above is that it forces you to write out all combinations of states/events in order to compile. So you would need to write something like:

const stateTransitions: StateMap = {
    [State.A]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    },

    [State.B]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    },

    [State.C]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    }
};

It depends a little on how you expect to implement the state map. Should it include every state as a top level key? If so then Record<State, ...> is a good place to start.

One of your requirements was:

Not every state-event combination will have a new state, sometimes the event does nothing.

You could cover this explicitly by stating that the event results in the same state. For example, if State A does not transition when Event B occurs then:

const stateTransitions: StateMap = {
  [State.A]: {
    [Event.B]: State.A
    // ...
  },
  // ...
}

Edit: Following on from the comments below this answer, if you want to exclude certain states from the state map because they are final states for example, you can do so as follows:

type StatesToInclude = Exclude<State, State.C | State.B>

type StateMap = Record<StatesToInclude, Record<Event, State>>

// ...
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement