Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data to states #12

Open
tp opened this issue May 24, 2016 · 6 comments
Open

Add data to states #12

tp opened this issue May 24, 2016 · 6 comments

Comments

@tp
Copy link

tp commented May 24, 2016

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?

@stefnotch
Copy link
Contributor

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

@eonarheim
Copy link
Owner

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

@stefnotch
Copy link
Contributor

stefnotch commented Mar 8, 2019

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.

@eonarheim
Copy link
Owner

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

@stefnotch
Copy link
Contributor

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
Copy link
Contributor

stefnotch commented Mar 10, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants