statelyai / xstate

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

raise actions cause TypeScript errors #1414

Closed AlCalzone closed 1 year ago

AlCalzone commented 4 years ago

Description I was asked to post this here (original comment) After I turned on strictFunctionTypes in my repo, TypeScript started complaining about raise actions in my transition definitions. In https://github.com/AlCalzone/node-zwave-js/blob/e10cb2c6d706bfc9dda0b5039ec38c651a2fbbd7/packages/zwave-js/src/lib/driver/SendThreadMachine.ts, you'll see I had to any some of the raise actions, e.g. line 394 or 405 (relevant commit)

Expected Result Since raise actions don't convey any context, I wouldn't expect them to cause incompatible types.

Actual Result If I remove the as any in line 405, this is the output from lerna run build:

zwave-js: src\lib\driver\SendThreadMachine.ts(383,4): error TS2322: Type '{ add: ({ cond: string; actions: any[]; } | { actions: (AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; })[]; message: ({ cond: string; actions: any; } | { actions: (_: any, evt: any) => void; })[]; serialAPIUnexpected: ({ cond: string; actions: any; } | { actions: (_: any, evt: any) => void; })[]; sortQueue: { actions: any[]; }; }' is not assignable to type 'TransitionsConfigMap<SendThreadContext, SendThreadEvent> | TransitionsConfigArray<SendThreadContext, SendThreadEvent> | 
undefined'.
zwave-js:   Types of property 'add' are incompatible.
zwave-js:     Type '({ cond: string; actions: any[]; } | { actions: (AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; })[]' is not assignable to type 'SingleOrArray<string | StateNode<SendThreadContext, any, { type: "add"; transaction: Transaction; }, { value: any; context: SendThreadContext; }> | TransitionConfig<SendThreadContext, 
{ type: "add"; transaction: Transaction; }> | undefined>'.
zwave-js:       Type '({ cond: string; actions: any[]; } | { actions: (AssignAction<SendThreadContext, { type: 
"add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; })[]' is not assignable to type '(string | StateNode<SendThreadContext, any, { type: "add"; transaction: Transaction; }, { value: any; context: SendThreadContext; }> | TransitionConfig<SendThreadContext, { type: "add"; transaction: Transaction; }> | undefined)[]'.
zwave-js:         Type '{ actions: (AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; }' is not assignable to type 'string | StateNode<SendThreadContext, any, { type: "add"; transaction: Transaction; }, { value: any; context: SendThreadContext; }> | TransitionConfig<SendThreadContext, { type: "add"; transaction: Transaction; }> | undefined'.
zwave-js:           Types of property 'actions' are incompatible.
zwave-js:             Type '(AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]' is not assignable to type 'string | ActionObject<SendThreadContext, { type: "add"; transaction: Transaction; }> | ActionFunction<SendThreadContext, { type: "add"; transaction: Transaction; }> | Action<SendThreadContext, { type: "add"; transaction: Transaction; }>[] | undefined'.
zwave-js:               Type '(AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]' is not assignable to type 'Action<SendThreadContext, { type: "add"; transaction: Transaction; }>[]'.
zwave-js:                 Type 'AssignAction<SendThreadContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'Action<SendThreadContext, { type: "add"; transaction: Transaction; }>'.
zwave-js:                   Type 'SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'Action<SendThreadContext, { type: "add"; transaction: Transaction; }>'.
zwave-js:                     Type 'SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'string'.
zwave-js: [19:44:16] 'compile' errored after 3.92 s

Note that some errors (including this one) are not shown in the editor or when building with --watch.

Reproduction See linked repo

Additional context

AlCalzone commented 4 years ago

I vaguely remember you wanting a CodeSandbox for this but I can't find that comment now. Is that still the case?

davidkpiano commented 4 years ago

Yes please!

AlCalzone commented 4 years ago

Mhh, I tried but it doesn't seem to work: https://codesandbox.io/s/nervous-panini-1tz0b

  1. I'm getting a weird error "e is undefined" in the first line in src/types.ts
  2. The error doesn't reproduce in the sandbox.
davidkpiano commented 4 years ago

I'll have to close this until there is a reproducible example. Perhaps you can make a more minimal example? I will reopen when that is available.

AlCalzone commented 4 years ago

I refactored the state machine recently and reached a much smaller repro. However I still can't get it to reproduce in the code sandbox or even my IDE. The error only appears when compiling from the CLI. If I remember correctly, this has something to do with different source file order in the language server vs the CLI.

If you can do with a reproducible example outside of the sandbox, then I recommend this:

  1. clone https://github.com/AlCalzone/node-zwave-js/tree/98c1e02ce32e91772759db044a22cc4bef717662
  2. npm install
  3. lerna run build - there should be no error
  4. Open packages/zwave-js/src/lib/driver/CommandQueueMachine.ts, go to line 127 (raise("trigger") as any,), remove the as any
  5. lerna run build - you'll get the following error:
    src/lib/driver/CommandQueueMachine.ts(105,2): error TS2322: Type 'StateMachine<CommandQueueContext, any, CommandQueueStateSchema, { value: any; context: CommandQueueContext; }>' is not assignable to type 'CommandQueueMachine'.
    The types of 'config.initial' are incompatible between these types.
    Type 'string | number | symbol | undefined' is not assignable to type '"idle" | "execute" | "abortSendData" | "executeDone" | undefined'.
      Type 'string' is not assignable to type '"idle" | "execute" | "abortSendData" | "executeDone" | undefined'.
    src/lib/driver/CommandQueueMachine.ts(118,4): error TS2322: Type '{ add: { actions: (AssignAction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; }; message: { actions: SendAction<any, any, any>; }; }' is not assignable to type 'TransitionsConfigMap<CommandQueueContext, CommandQueueEvent> | TransitionsConfigArray<CommandQueueContext, CommandQueueEvent> | undefined'.
    Types of property 'add' are incompatible.
    Type '{ actions: (AssignAction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]; }' is not assignable to type 'SingleOrArray<string | StateNode<CommandQueueContext, any, { type: "add"; transaction: Transaction; }, { value: any; context: CommandQueueContext; }> | TransitionConfig<CommandQueueContext, { type: "add"; transaction: Transaction; }> | undefined>'.
      Types of property 'actions' are incompatible.
        Type '(AssignAction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]' is not assignable to type 'string | ActionObject<CommandQueueContext, { type: "add"; transaction: Transaction; }> | ActionFunction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | Action<CommandQueueContext, { type: "add"; transaction: Transaction; }>[] | undefined'.
          Type '(AssignAction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>)[]' is not assignable to type 'Action<CommandQueueContext, { type: "add"; transaction: Transaction; }>[]'.
            Type 'AssignAction<CommandQueueContext, { type: "add"; transaction: Transaction; }> | RaiseAction<EventObject> | SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'Action<CommandQueueContext, { type: "add"; transaction: Transaction; }>'.
              Type 'SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'Action<CommandQueueContext, { type: "add"; transaction: Transaction; }>'.
                Type 'SendAction<unknown, AnyEventObject, EventObject>' is not assignable to type 'string'.
davidkpiano commented 4 years ago

@AlCalzone Ah I see, inference isn't perfect here. The correct typing should be:

raise("trigger") as RaiseAction<CommandQueueEvent>

Looking at better ways to resolve inference for these (cc. @Andarist)

jlarmstrongiv commented 1 year ago

Yep, I was dealing with this issue in one of the xstate tutorials.

The raise("SKIP") as RaiseAction<{ type: "SKIP" }> did work, but, unfortunately, it shows as an inline function in the stately editor (which is a different bug tracked here).

Code example Adapted from https://github.com/davidkpiano/frontend-masters-xstate-v2/blob/main/03-context/main.final.js ```ts // @ts-check import "../style.css"; import { createMachine, assign, interpret } from "xstate"; import elements from "../utils/elements"; import { raise, send, sendTo } from "xstate/lib/actions"; import type { RaiseAction, SendAction } from "xstate"; import { formatTime } from "../utils/formatTime"; type PlainEvents = EventNames extends string ? { type: EventNames } : never; type FSAEvent = Payload extends {} ? { type: Type; payload: Payload } : { type: Type }; enum LikeStatus { liked = "liked", unliked = "unliked", disliked = "disliked", } type PlayerMachineContext = { title?: string; artist?: string; duration: number; likeStatus: LikeStatus; volume: number; elapsed: number; }; type VolumeEvent = FSAEvent<"VOLUME", { level: number }>; type LoadedEvent = FSAEvent< "LOADED", { title: string; artist: string; duration: number; } >; type EventNames = "PLAY" | "PAUSE" | "DISLIKE" | "SKIP" | "UNLIKE" | "LIKE"; type Events = PlainEvents | VolumeEvent | LoadedEvent; const playerMachine = /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgGUBpASQAUBtABgF1FQAHAe1lwBdcP8rEAA9EANgDsAGhABPRACYAHGIB0ATgkMJ6pQBZ1CvQGYAjGL0BfSzLRY8hUgBkqFAKKMWSEJ258BQqIIeqZKqgxK2qHq6iF6DGIy8gimEarmxvFixhIK2gxm1rYYOATEJACqAHIu7p5Cvrz8gt5BIQwaxgympnoSemIM6jkArEmIpgpFIHaljiQAIlRktR7MDVxNAa2I8eqqYqa6hpMFh8bjCCN6CqoSpiPqEQwjLzmm07MO5QBqAPJOCoAWTWXnYm38LVAbQYtxUI2MOXeSgUYgUlz0A1UIxGCle5gk+jEYiUnxK3yIqgANhx0BACFASE4-gBBBZuBb1byNSGBRBdMK4iTCobqQbDCRjOSKFQaLQ6fSGEzmKw2GbksqUtjoACusEgJBoThZAE0ueC-M0+QgSXpsWJYgp+k8FJk9JcFIZwsSLAiGCY0WT7JrVGwqehZAzDSyKmRQRtLdtoeJ+qplJ7+kYzBZTJdMrdripjK6CkoJDlrGr8BwIHAhF9NQmtlCRBN1JdQmpiySHmKlGKfUG5sRqbT6fgoE3eTsEHlTOEEtpPbp-SSlHnUqouoMlMZJOojoYhxTQ7r9RAp1aZ4S1CFYvd+kunXmGB0LIjBqYcsNYiNjyGwwjBlLyTVtgl3cJhQGJQyzyLQLmlWdy1UJRTB0L8XmuQ9K0sIA */ createMachine({ schema: { context: {} as PlayerMachineContext, events: {} as Events, }, preserveActionOrder: true, tsTypes: {} as import("./main.final.typegen").Typegen0, initial: "loading", context: { title: undefined, artist: undefined, duration: 0, elapsed: 0, likeStatus: LikeStatus.unliked, // or 'liked' or 'disliked' volume: 5, }, states: { loading: { on: { LOADED: { actions: "assignSongData", target: "playing", }, }, }, paused: { on: { PLAY: { target: "playing" }, }, }, playing: { entry: "playAudio", exit: "pauseAudio", on: { PAUSE: { target: "paused" }, }, }, }, on: { SKIP: { actions: "skipSong", target: "loading", }, LIKE: { actions: "likeSong", }, UNLIKE: { actions: "unlikeSong", }, DISLIKE: { actions: [ "dislikeSong", raise("SKIP") as RaiseAction<{ type: "SKIP" }>, ], }, VOLUME: { actions: "assignVolume", }, }, }).withConfig({ actions: { assignSongData: assign({ title: (context, event) => event.payload.title, artist: (context, event) => event.payload.artist, duration: (context, event) => event.payload.duration, elapsed: (context) => 0, likeStatus: (context) => LikeStatus.unliked, }), likeSong: assign({ likeStatus: (context) => LikeStatus.liked, }), unlikeSong: assign({ likeStatus: (context) => LikeStatus.unliked, }), dislikeSong: assign({ likeStatus: (context) => LikeStatus.disliked, }), assignVolume: assign({ volume: (context, event) => { return event.payload.level; }, }), // assignTime: assign({ // elapsed: (_, e) => e.currentTime, // }), skipSong: () => { console.log("Skipping song"); }, playAudio: () => {}, pauseAudio: () => {}, }, }); const service = interpret(playerMachine).start(); // @ts-expect-error window.service = service; // @ts-expect-error elements.elPlayButton.addEventListener("click", () => { service.send({ type: "PLAY" }); }); // @ts-expect-error elements.elPauseButton.addEventListener("click", () => { service.send({ type: "PAUSE" }); }); // @ts-expect-error elements.elSkipButton.addEventListener("click", () => { service.send({ type: "SKIP" }); }); // @ts-expect-error elements.elLikeButton.addEventListener("click", () => { service.send({ type: "LIKE" }); }); // @ts-expect-error elements.elDislikeButton.addEventListener("click", () => { service.send({ type: "DISLIKE" }); }); service.subscribe((state) => { console.log(state.context); const { context } = state; // @ts-expect-error elements.elLoadingButton.hidden = !state.hasTag("loading"); // @ts-expect-error elements.elPlayButton.hidden = !state.can({ type: "PLAY" }); // @ts-expect-error elements.elPauseButton.hidden = !state.can({ type: "PAUSE" }); // @ts-expect-error elements.elVolumeButton.dataset.level = context.volume === 0 ? "zero" : context.volume <= 2 ? "low" : context.volume >= 8 ? "high" : undefined; // @ts-expect-error elements.elScrubberInput.setAttribute("max", context.duration); // @ts-expect-error elements.elScrubberInput.value = context.elapsed; // @ts-expect-error elements.elElapsedOutput.innerHTML = formatTime( context.elapsed - context.duration ); // @ts-expect-error elements.elLikeButton.dataset.likeStatus = context.likeStatus; // @ts-expect-error elements.elArtist.innerHTML = context.artist; // @ts-expect-error elements.elTitle.innerHTML = context.title; }); service.send({ type: "LOADED", payload: { title: "Some song title", artist: "Some song artist", duration: 100, }, }); ```
davidkpiano commented 1 year ago

This may be addressed by https://github.com/statelyai/xstate/pull/3694