TypeState icon indicating copy to clipboard operation
TypeState copied to clipboard

Add data to states

Open tp opened this issue 9 years ago • 6 comments

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:

enum AirplaneBoarding {
  case Pending
  case Ongoing(peopleBoarded: number)
  case Finished
}

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?

tp avatar May 24 '16 11:05 tp

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 avatar Mar 07 '19 16:03 stefnotch

@stefnotch Cool! Open a PR, is there a way to preserve the existing behavior with to avoid a breaking change?

eonarheim avatar Mar 08 '19 02:03 eonarheim

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):

https://github.com/stefnotch/TypeState/blob/266fa325600dd0f5cd034235347aa02a01012ed4/src/typestate.ts#L77

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 avatar Mar 08 '19 08:03 stefnotch

@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:

  • Introduce this breaking change but rev major version in semver (hesitant given the Enum precedent)
  • Introduce a new path through the code possibly a new constructor, overload, or new method 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> {
...
}
  • Find a sufficient type describer that works for both enums and objects. (may not be possible)
  • Alternately this could be implemented outside of TypeState as a recipe/template for associating states with statically typed metadata.

eonarheim avatar Mar 10 '19 18:03 eonarheim

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];

stefnotch avatar Mar 10 '19 20:03 stefnotch

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

stefnotch avatar Mar 10 '19 20:03 stefnotch