statelyai / xstate

Actor-based state management & orchestration for complex app logic.
https://stately.ai/docs
MIT License
26.79k stars 1.23k forks source link

Guarantee State Change Statically? #170

Closed jon49 closed 5 years ago

jon49 commented 6 years ago

Bug or feature request?

Feature Request

Description:

Starting the example on the first page I noticed that lightMachine.transition(lightMachine.initialState, "TIMER").value Does not guarantee that the initial state (or any state for that matter) nor the transition are guaranteed to be a name of the object which was passed in the Machine constructor. It would be nice if we could have a static guarantee.

(Bug) Expected result:

lightMachine.transition("green1", "TIMER_505").value

Should have a compile error saying green1 is not a state and TIMER_505 is not a transition.

(Bug) Actual result:

No compile error.

(Bug) Potential fix:

I had a question about something similar to this on Stack Overflow that I think might help. Granted I noticed in your code base that you have similar constructs so I'm not sure if something like this was already attempted.

https://stackoverflow.com/a/48695832/632495

As I don't have a ton of free time. I'm afraid I will probably not be of much help. But this is an exciting project! I hope you will be able to monetize it!

(Feature) Potential implementation:

Required! If you would like to see a feature or enhancement make its way to xstate, please describe:

Link to reproduction or proof-of-concept:

https://codepen.io/jon49/pen/wEBXyK

jon49 commented 6 years ago

Here's the beginning of an implementation. I haven't finished it yet (I have to get to bed), but you can see where it is going. It is a naive implementation of course, more of a proof of concept.

namespace Test {

    type ActionType =
        { on: { [transitionName: string]: string } }
      | { [actionName: string]: { [transitionName: string]: string } }

    interface Transition<TransitionName = string, TParentAction = string, TParentState = string> {
        Name: TransitionName
        ParentAction: TParentAction
        ParentState: TParentState
    }

    interface Action<Transitions extends { [name: string]: Transition } = {}, ParentState = string, ActionName = string> {
        Name: ActionName
        Transitions: Transitions
        ParentState: ParentState
    }

    interface State<
            StateName,
            Actions extends { [name: string]: Action }> {
        Name: StateName
        Actions: Actions
    }

    function state<StateName extends string, TActions extends { [name: string]: ActionType }>(
        stateName: string, actions: TActions
    ): State<StateName, { [P in keyof TActions]: Action<> }> {

    }

}
davidkpiano commented 6 years ago

But this is an exciting project! I hope you will be able to monetize it!

Thanks! xstate will always be free and open-source software. I'm currently working on building tools around it that and a base set of features that will also remain free for open-source projects 😉

I've thought about strictly typing state/action names, but there's a couple issues, mainly due to the string-based names, both current and future:

And most of these are hard/impossible to statically type. With that said, I will be working on dev-time analysis tools that can detect improperly formed statecharts at runtime, similar to how e.g., React detects two colliding element keys, which is impossible to do at compile time.

jon49 commented 6 years ago

Yeah, you definitely have the higher level view of everything that needs to be done. I would argue that it should be possible to do all the different things. But maybe not overloading the method parameters might be a good way to go about it. I know in JS overloading methods is common but in statically typed languages it typically isn't done because you do end up getting clashes and so you are forced to write new methods for each use case.

So, I think it is possible. Whether it is feasible with the amount of resources that you have might be a different story.

But thanks for considering it and I hope that it might eventually make it into the code base. Having static typing do much of the "testing" is a nice way to go about writing code. I think that is the main reason I like doing it that way. Then I don't have to write quite so many tests.

Thanks again!

davidkpiano commented 6 years ago

@jon49 Thinking about this a little further... what if there were some command-line program that generated type definitions from an xstate JSON specification?

I.e., if you had something like:

lightMachine.json

{
    "id": "light",
    "initial": "green",
    "states": {
        "green": { "on": { "TIMER": "yellow" } },
        "yellow": { "on": { "TIMER": "red" } },
        "red": { "on": { "TIMER": "green" } }
    }
}

It would generate something like...

namespace LightMachine {
  export type Events = 'TIMER';
  export type States = '#light.green' | '#light.yellow' | '#light.red';
  export interface RelativeStates = {
    light: 'green' | 'yellow' | 'red';
  }
  export type Ext = undefined; // external state

  // StatechartDefinition would be imported from xstate
  export interface Definition = StatechartDefinition<States, Events, Ext>;
}

And then that can be immediately used in the project somehow. Still thinking about this.

jon49 commented 6 years ago

That would work. Then the state machine would be more of a configuration file and easier to share between programs. I think you could do it all in code with TypeScript. But this might be a better solution since you could the share the file.

You could have a file lightMachine.js (or ts, or yaml, or ...) and export it to json too. Then it would be more of an optional workflow.

Generating also gives you the advantage of having tests which occur during the generation. And you could use JsonSchema or something like that to type check your JSON.

Lot's of possibilities!

carloslfu commented 5 years ago

I was exploring solutions to this problem and code generation seems to be the better, another useful thing we can get is to be able to type the actions. That would look like:

import { Machine } from 'xstate'
import statechart from './statechart.json'
import StatechartType from './statechart.ts'

const m = Machine<StatechartType>(statechart)

type Actions = {
  [name in StatechartType.Actions]: { (): void }
}

const actions: Actions = {
  action1: () => {},
  action2: () => {},
}

// ... interpreter stuff
jon49 commented 5 years ago

Just got a chance to work with this library and notice that 4.0.x was out and it seems you guys created the feature! I'll close this one for you.

carloslfu commented 5 years ago

Yes, it is ready. The missing thing here is an XState Config to TS generator, it could be built in a separate package. I can start building it once I have free time. If someone is working on it, please give us a heads up here :).