Xstate: [4.10: TypeScript] Typed actions throw type errors

Created on 21 May 2020  路  13Comments  路  Source: davidkpiano/xstate

Description
Hello everybody,
I just upgraded to version 4.10 ready to fix my send types. I'm so happy about this release! Thank you so much for introducing the third generic toSendEvent. I oftentimes had to go for <any, any> as a workaround and am so glad to finally change those. 馃枻

When defining an assign action within the configuration object, TypeScript goes nuts.

actions: {
  assignCard: assign<MContext, SubmitEvent>({
    card: (_, event) => event.data.name
  })
}

I got the same error when passing the action to machine.withConfig().

Expected Result
No type error should be thrown.

Actual Result
TypeScript error pyramid of death 馃槵

image

Reproduction
https://codesandbox.io/s/xstate-bug-typed-assign-action-in-configuration-object-5pc63?file=/src/index.tsx:1858-1981

Additional context

@xstate/react: 0.8.1,
xstate: 4.10.0
bug typescript

Most helpful comment

Workaround:

    actions: {
      assignCard: assign<MContext, any>({
        card: (_, event) => (event as SubmitEvent).data.name
      })
    }

All 13 comments

Workaround:

    actions: {
      assignCard: assign<MContext, any>({
        card: (_, event) => (event as SubmitEvent).data.name
      })
    }

(event as SubmitEvent).data.name
I didn't know this worked in TypeScript 馃槻

Or a different workaround, imo slightly nicer :)

actions: {
  assignCard: assign<MContext, SubmitEvent>({
    card: (_, event) => event.data.name
  }) as any // <-- this
}

I assume this will get better in V5?

I assume this will get better in V5?

That's the goal

Will this be only fixed when V5 hits? I have 100+ type errors because of this and am contemplating if it's worth to fix them with the described workaround.

I was hoping to upgrade to 4.10 not only to fix my send types but also because pure and choose actions did not work correctly with the machine configuration object in 4.9.1.

@CodingDive I've extended my workaround and I basically have all my actions for the machine declared in a separate file, exporting them in a single object, and then in machine config I can do just this. It's easy to find later and remove.

{
  actions: myMachineActions as any
}

The downside is that you have to explicitly specify generics for Context and Events for each action, but that's bearable imo.

This is the point in time where I regret having quite a few inline-actions 馃榿

The issue also seems to affect some send/sendParent actions

sendToChild: send<
  Context,
  CurrentEvent,
  EventToBeSent
>('SOME_EVENT', {
  to: (context, event) =>
    context.items.find(({ id }) => id === event.id)?.ref,
}),

Makes the compiler throw a pretty long error

Type of computed property's value is 'SendAction<Context, CurrentEvent, EventToBeSent>', which is not assignable to type 'ActionObject<Context, AllEvents> | ActionFunction<Context, AllEvents>'.
  Type 'SendAction<Context, CurrentEvent, EventToBeSent>' is not assignable to type 'ActionObject<Context, AllEvents>'.
    Types of property 'exec' are incompatible.
      Type 'ActionFunction<Context, CurrentEvent> | undefined' is not assignable to type 'ActionFunction<Context, AllEvents> | undefined'.
        Type 'ActionFunction<Context, CurrentEvent>' is not assignable to type 'ActionFunction<Context, AllEvents>'.
          Types of parameters 'meta' and 'meta' are incompatible.
            Type 'ActionMeta<Context, AllEvents>' is not assignable to type 'ActionMeta<Context, CurrentEvent>'.
              Types of property '_event' are incompatible.
                Type 'Event<AllEvents>' is not assignable to type 'Event<CurrentEvent>'.
                  Type 'AllEvents' is not assignable to type 'CurrentEvent'.
                    Type 'ISomeOtherEvent' is not assignable to type 'CurrentEvent'.

The fix here is the same as the assign fix: Use any.

sendToChild: send<
  Context,
  CurrentEvent,
  EventToBeSent
>('SOME_EVENT', {
  to: (context, event) =>
    context.items.find(({ id }) => id === event.id)?.ref,
}) as any,

The main problem is that TypeScript often has a hard time inferring object values when it can be one of many things. Looking into a few ways to remedy this at the TS level.

It also seems to behave non-deterministic. It would sometimes show 0 errors then list quite a few when recompiling without changing anything.

Another TypeScript related problem is that when one of the machines has a type error, it also shows errors when spawning the machine. If an actor is spawned by three machines but also has a type error, it would sometimes print 4 errors. One for the machine that actually has the type errors and for the 3 parents when spawning the machine. I can't remember this happening with < 4.10 but I also wasn't on the latest TypeScript back then.

@davidkpiano I ran into this as well and while trying to find a good workaround, I actually wonder if it wouldn't be more accurate for actions to not depend on the transition events calling them, as different actions may share the same actions.

I'd expect assign() to be a generic accepting <Context, { expectedProp: number }> so that the action defines what props transition events must have to be allowed to call this action.

@FokkeZB Can you share a code example of the problem you're running into?

I'd expect assign() to be a generic accepting <Context, { expectedProp: number }> so that the action defines what props transition events must have to be allowed to call this action.

That's an interesting idea. Would have to explore how to best do that at the type level.

Note that you can already sort-of do this:

<Context, Extract<Events, { expectedProp: number }>>

I don't think we should support this out of the box - current types are already complex and this would only raise the complexity. Offloading this to the user space is also a more robust solution as one could write any type matcher on their own, instead of us enforcing a specific type of such matcher.

@Andarist I like that pattern, as with that you indicate you don't care what events call the action, as long as they have certain props. You could even do the same for Context if we'd have typing of what it may look like for each state, just like we do for Events.

@davidkpiano it's not so much of a problem, but a way to think about types. We've run into this with GraphQL where initially we'd use the generated types for queries in presentational components. That may make sense initially, but ultimately breaks you up. Types for output and input should be duplicated as it shouldn't be of a component/function's concern where the data comes from (and what additional props might be passed along), but just define what it needs.

OK, one example. If I'd follow the last snippet at https://xstate.js.org/docs/guides/context.html#typescript and add a new transition that calls the same action (which should then of course not be defined inline) with the same payload, I now need to update the action's typing even though nothing has changed about the action's needs. This is of course, assuming the action doesn't use the event's type prop, only the custom ones.

Was this page helpful?
0 / 5 - 0 ratings