Open tp opened 8 years ago
I'm currently working on a fork that can do something like that while keeping all the magical type checking.
So far, the main differences are that you use a class instead of an enum
class Elevator {
DoorsOpened = {a:1};
DoorsClosed = {b: 3};
Moving = "Henlo"
}
of which an instance has to be passed to the constructor
var fsm = new typestate.FiniteStateMachine(new Elevator(), "DoorsClosed");
and instead of writing Elevator.DoorsOpened
, you just write strings (which do have type checking!)
fsm.from("DoorsOpened").to("DoorsClosed");
https://github.com/stefnotch/TypeState/blob/master/example/example.ts
@stefnotch Cool! Open a PR, is there a way to preserve the existing behavior with to avoid a breaking change?
I think it should be possible to make the existing enum
behaviour work, however not without a fair bit of work thanks to TypeScript's enum behaviour.
There is another breaking change as well. The callback now have an optional second parameter which is the current context. e.g.
public on<U extends keyof T>(state: U, callback: (from?: keyof T, context?: T[U], event?: any) => any):
This could be fixed by making the context
the last parameter, however I think that usually you want the previous state + the context
and not the event
. Which is why I left the event
as the last parameter.
@stefnotch I see, typescript enums are certainly cumbersome. I need some more time to think about this, given the length of time TypeState has relied on enums for defining states I want to be careful about how to proceed.
A couple options I see right now:
withData
or withContext
which would return the data associated with the state, and potentially avoid the breaking change, and provide the new desired functionality.
public withData<U extends keyof T>(state: U, cb: (data: T[U]) => any): FiniteStateMachine<T> {
...
}
I looked into this a bit more. I have found a type that sort of works for classes and enums.
However, it forces you to specify everything as "strings"
instead of the typical Enum.DotNotation
enum Swag {
Yo,
Yolo,
Nope
}
var x: keyof typeof Swag;
x = "Yolo";
class SwagClass {
static "Yo": { hello: 1 };
}
var y: keyof typeof SwagClass;
y = "Yo";
I also looked into associating a context with an enum in a typesafe way. However, the approach I found isn't quite as pretty as I'd like
enum Swag {
Yo,
Yolo,
Nope
}
const SwagContext = {
[Swag.Yo]: { hello: 1 },
[Swag.Yolo]: "x",
[Swag.Nope]: null
};
// Now the user is forced to specify something for `K`
class FiniteStateMachine<T, K> {
context: K;
constructor(startState: T, context?: K) {
this.context = context;
}
}
var fsm = new FiniteStateMachine<Swag, typeof SwagContext>(
Swag.Yolo,
SwagContext
);
// It works with type checking
fsm.context[Swag.Yo];
Regarding the sufficient type description, it might be possible to use conditional types to cover both cases (enums and classes).
Here is an example of conditional types.
type ConditionalTest<T> = T extends object ? number : string;
enum Swag {
Yo,
Yolo,
Nope
}
let y: ConditionalTest<Swag>; // string
class SwagClass {}
let x: ConditionalTest<SwagClass>; // number
This is more of a question/feature request (or at least basis for discussion of such):
Is there any standard way to add data to a state, or would you be interested in discussing such a feature?
I think in many business cases it would be useful to attach some data to a state as opposed to creating a multitude of individual states (which not practical if there are many "sub-states").
Sadly TypeScript does not support data in enum cases, so I don't see a straightforward way yet to implement this in TypeScript/TypeState. In Swift or Rust it is a core feature to add data to enum cases, so under such circumstances no special casing would be needed.
An example of a state machine that would be more useful with added data would be:
Do you have any implementation hints on how to approach something like the above? Or is this something that should not attempted when working with FSMs for some reason?