> I personally think your example is convoluted for such a simple example, but that's just me.

It is indeed all a matter of taste in the end, but I mean it's the exact transcription of a textbook FSM. I don't think your example would get that much simpler with a stable identifier, which brings me to:

> I don't have to create a monolithic state transition table logic

It doesn't have to be monolithic. I just created a monolithic function because it's easier for this small example. It could just as easily dispatch on state (which is what you're doing here), or on transition (which is impossible with your hierarchy), or mix and match them. I can write those examples if you're curious.

Your class example is a good example of a state machine trace (e.g. one that you might often use to create an ephemeral config object), but it's not an example of an actual persistent state machine. As soon as you use a stable identifier the type safety goes out the window.

let stateMachine = new On();
// Wait a bit for the user to do something
stateMachine = statemachine.turnOff();
// Wait again for the user to do something
stateMachine = stateMachine.turnOn();

Depending on how you annotate the initial let, one of those two lines will cause TS to complain (or you have to do a manual cast somewhere which circumvents the type safety).

Indeed if you just care about a state machine trace the same type safety holds in the explicit state transition table case if you just use TS's literal types in the reducer's type annotations. You just explicitly call the reducers you need however many times and then persist the state machine at the very end.

That is your type safety doesn't come from the representation as an object, but rather that there is no stable state machine between invocations, but only an ephemeral trace of one whose intermediate states are immediately destroyed. Traces are valuable! But you could generate the exact same trace with an explicit state transition function and you would get the exact same type safety that way. I can write it out for you if you're curious.

> I will never have to worry about the combinatorial explosion that can happen in a global transition table.

Again you don't have to worry about a combinatorial explosion in a global transition table either if you dispatch on actions or states.

The fundamental difference between what you've outlined here and the explicit state transition table is that in your approach states are first-class and transitions are not (but rather methods attached to states), whereas the explicit state transition function treats both of them as first-class entities.

For places where you really only need an ephemeral state machine trace, rather than a state machine that persists between calls, you don't need first-class transitions because the transitions are all ephemeral and cannot be dynamically called at runtime.

Where you do have users able to dynamically call transitions at runtime, you end up needing to represent those transitions somehow, and you end with something approaching Redux (indeed I think an interesting exercise would be to implement On and Off where the user either presses "a" or "b" at the keyboard to turn on and off with your classes. I suspect you end up with just the Redux approach all over again).

EDIT: I want to emphasize I don't think Redux is free of faults. There are many things I really dislike about it and the React ecosystem it integrates into. But I don't think any of that can be chalked up to a poor representation of a state machine.