Bug
I define an action in machine configuration. The action should be executed after a promise is resolved. The action is defined like this.
setResult: actions.assign<FetchContext, DoneInvokeEvent<MessageStatus>>((ctx, e) => {
Typescript then give me an error saying that "MyEvent is not assignable to DoneInvokeEvent
Compiles ok!
Compilation error
nominal typing?!?
Here is a gist with code that shows the problem.
Hi @erizet
Correct me if I am wrong, but wouldn't be the case of just editing in your gist the following line?
type FetchEvent = FetchEventWithMessageId | FetchEventWithMessageStatus | DoneInvokeEvent<MessageStatus>;
After editing it I have no problem with the typings anymore.
Hi @assuncaocharles
Your observation is correct, but I was told to open this issue by @davidkpiano.
This is from our conversation Gitter.
@erizet So, this really needs nominal typings (opaque/unique strings) in TypeScript, but there is a good workaround:
interface MyDoneEvent{ type: 'done.invoke'; data: T } type FetchEvent =
FetchEventWithMessageId
| FetchEventWithMessageStatus
| MyDoneEvent; // ...
setResult: actions.assign((ctx, e: MyDoneEvent
) => {
return { messageStatus: e.data };
}),
We're basically saying that even though the done event is something like 'done.invoke.whatever', we'll coerce it to that specific 'done.invoke' value for now so that the discriminated unions work correctly
Can you open an issue with this? https://github.com/davidkpiano/xstate/issues/new I have a "temp" fix for it so that DoneInvokeEventworks as-is
(using the same fix as above)
Slight problem with that solution is that it doesnt handle multiple, different, done events in a single machine - no idea how to solve this in TS right now though
Slight problem with that solution is that it doesnt handle multiple, different, done events in a single machine - no idea how to solve this in TS right now though
I'm having this constraint right now, and using the union with | DoneInvokeEvent<MessageStatus> makes the events type string , limiting my events types.
Agreed. Having an event like DoneInvokeEvent where the type could be _anything_ makes it super hard to handle the event object in other contexts (for example, analyzing the result of onDone in a guard, or retrieving the event object in a service).
Specifying the dummy type done.invoke makes the type checker happy without any runtime errors. But under the hood, this type isn't actually what appears at runtime. In the future, enforcement of InvokeEvent types would be super helpful!
This is a good use case for a codegen tool to generate the required types. I've got a demo working in this package, albeit buggy.
Introspecting the machine and divining the connections between all of these services and actions would lead to some pretty awesome type inferences. I've added an example output below.
https://gist.github.com/mattpocock/f70734aa596a97355cc30a01bceb2539
Note that you can get perfect typechecking on all the data payloads of done.invoke.<serviceName> by assigning their types in the event type you pass as a generic into Machine. It extracts which events cause which services, so casting (event.data as any) is no longer required.
Just a note (for my reference too), the event is properly inferred inline:

One of the problems is that actions (as well as guards, etc.) defined in the machine options (second arg of createMachine(..., options)) are assumed to occur in _any state_, which is why we can't safely infer that a DoneInvokeEvent can occur in some arbitrary transition(s) (even though it might be clear in the machine config that it's only used within an invoke block).
Again, not an easy (much less possible) problem to solve without a codegen like what @mattpocock is working on. Explicitly defining what kinds of events an action in option.actions expects is probably the best thing to do here.
Alternatively, you can define the assign action inline. It's already serializable there.
I have the actions written in a diff obj, I opted to not use assign<Context, Event>({}) but just inline the event assign<Context, DoneInvokeEvent<...>>({}) in the case of DoneInvokeEvent actions, use the Event type for all the others, callback also has like a nicer interface than invoke Promise, as it just raises an event, and its all cake from there.
Most helpful comment
This is a good use case for a codegen tool to generate the required types. I've got a demo working in this package, albeit buggy.
Introspecting the machine and divining the connections between all of these services and actions would lead to some pretty awesome type inferences. I've added an example output below.
https://gist.github.com/mattpocock/f70734aa596a97355cc30a01bceb2539
Note that you can get perfect typechecking on all the data payloads of
done.invoke.<serviceName>by assigning their types in the event type you pass as a generic intoMachine. It extracts which events cause which services, so casting(event.data as any)is no longer required.