mattpocock / xstate-codegen

A codegen tool for 100% TS type-safety in XState
MIT License
245 stars 12 forks source link

service invocation question #33

Closed mwarger closed 1 year ago

mwarger commented 4 years ago

Let's say I have a service definition like this:

initializing: {
            invoke: {
              src: 'getToken',
              onDone: {
                actions: 'updateToken',
                target: 'ready',
              },
              onError: {
                target: 'failure',
              },
            },
          },

I want to update the token in context when I am done. Currently, this requires me to have an event declared like this:

{
      type: 'done.invoke.getToken';
      data: string;
    }

(if I change the ID, the event would change, afaik)

This is used in the event of the action:

actions: {
      updateToken: assign((context, event) => { // this event is typed correctly, but only if I create the event explicitly in the Event type for my machine
        console.log('event', event.data);
        return {
          authorization: event.data,
        };
      }),
    },

First, I'm curious if this is the correct usage? Second, I'm curious if this could be inferred somehow by properly crafting the type string for invoked services (as they follow a convention)? This could potentially allow me to not need to create the event explicitly.

mattpocock commented 4 years ago

@mwarger You're absolutely right to follow this pattern. It will give you 100% type safety, but I'm a bit reticent to call it a good pattern. It requires that you know about XState's internals, which ain't right.

You can also do the same for error.platform.service, and various others.

I have had similar doubts about this, but haven't thought of an API which would make sense.

We'll likely need some kind of generic passed into the Machine. Perhaps:


type Options = {
  services: {
    getToken: {
      data: string;
    };
  };
};

const machine = Machine<Context, Event, Options, 'id'>({});

This is tricky, though - we are committed to supporting Typestates (in #27) which already makes use of this extra generic.

The floor is open - I'm open to ideas here.

mwarger commented 4 years ago

I don't know what I'm talking about, but this approach might be useful here if we can leverage the convention and extract keys from the config object: https://twitter.com/danvdk/status/1301707026507198464

mattpocock commented 3 years ago

Yesss, this should definitely be explored when TS 4.1 is out.

mwarger commented 3 years ago

I was thinking more about this as I was falling asleep. It's that followup by Jamie that I think might be close.

ts playground here

Could we create one big type that models a state machine? Then extract all the types from the parsed one? I'm not sure if it works that way... Would love to talk this through with people that are more familiar with it.

(and then I go back to the twitter thread and scroll down and see that you've already commented in there with David - I'm glad it's on your radar!)

mattpocock commented 3 years ago

This would be particularly interesting because we could re-parse the done.invoke.serviceName into something more readable. For instance, serviceName.onDone or serviceName.onError could be passed to your Event object. This would be a fairly small change, though would mean we'd need to require TS 4.1+ for the CLI to be used.

mattpocock commented 3 years ago

@mwarger We could actually do the above without the TS string manipulation. When we encounter something that triggers from the done.invoke.serviceName event, we could do the string manipulation ourselves to allow for either that, or serviceName.onDone to be passed in the Events object.

danielkcz commented 3 years ago

Just bumped into this as well. I wonder, technically when the service is declared inline or within machine options, it should be possible to infer return value and have the onDone typed correctly, shouldn't it? The problem is of course for services configured externally and that probably needs something of what you are suggesting above.

I also tried to specify ev: DoneInvokeEvent<TData> explicitly in action, but it's not compatible and complains about it for whatever reason. Otherwise, it would be a semi-nice solution.

mattpocock commented 3 years ago

@FredyC What do you think of the above fix, to declare events like this to get onDone typed correctly?

{
  type: 'done.invoke.getToken';
  data: string;
}
danielkcz commented 3 years ago

Well, for some reason that didn't work for me, I am getting any, but my data is an object, not a scalar value if that matters. I think it's ugly anyway :) It would be much better if at least partial inference would be available for internal services as I suggested. And figure out how can I use DoneInvokeEvent explicitly without clashing with something I don't understand.

mattpocock commented 3 years ago

@FredyC Could you post a repro? It shouldn't matter whether it's scalar or not.

It is certainly not pretty. Inference of service payloads inline is a long way off, so it might have to do for now.

mattpocock commented 3 years ago

I'm going to keep this open but mark it as documentation needed - we should document this approach so it's more discoverable.