statelyai / xstate-tools

Public monorepo for XState tooling
183 stars 36 forks source link

[Typescript] when using typegen, type error for missing implementations does not tell you what is missing #302

Open johtso opened 2 years ago

johtso commented 2 years ago

Description

Currently if your machine references an action/guard/service that is not defined and you try to interpret it, you get an error like:

Argument of type 'StateMachine<{ worker: WorkerHttpvfs | null; mode: "random" | "randompopular" | "randomoverlooked" | "popular" | null; cursor: Cursor | null; images: Image[]; gallery: Masonry<...> | null; fetchQueue: number[]; }, ... 5 more ..., ResolveTypegenMeta<...>>' is not assignable to parameter of type '"Some implementations missing"'.ts(2345)

On a machine of a manageable size you can go through your machine line by line and check that a definition exists for each thing referenced. On a larger machine this could become a real chore.

Is there a possibility of outputting what specific implementations are missing?

Expected result

A type error that tells you what's missing from your machine.

Actual result

A general type error that just tells you that something is missing.

Reproduction

https://codesandbox.io/s/trusting-meninsky-95sj4r?file=/src/index.ts

Additional context

No response

Andarist commented 2 years ago

Since interpret is exported from the root entry we could do this with the help of typesVersions.

We could use an improved version of this:

export declare function interpret<
  TContext = DefaultContext,
  TStateSchema extends StateSchema = any,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = {
    value: any;
    context: TContext;
  },
  TResolvedTypesMeta = TypegenDisabled
>(
  machine: AreAllImplementationsAssumedToBeProvided<TResolvedTypesMeta> extends true
    ? StateMachine<
        TContext,
        TStateSchema,
        TEvent,
        TTypestate,
        any,
        any,
        TResolvedTypesMeta
      >
    : `Missing: ${Cast<
        Values<
          Prop<Prop<TResolvedTypesMeta, "resolved">, "missingImplementations">
        >,
        string
      >}`,
  options?: InterpreterOptions
): Interpreter<TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta>;

This one produces a union of strings, so in the error tooltip you will only see information about a single missing implementation (instead of having a list of all the missing implementations) so after fixing such a problem you would have to circle back to the error to learn what else is missing. It's better than nothing but not ideal.

Based on this beautiful (but scary) SO answer we can concert a union to a tuple and a tuple of strings can be converted to a single string with smth like:

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;

type ConcatTuple<T extends string[]> = T extends []
  ? ""
  : `${Head<T> & string}, ${Tail<T> extends infer R
      ? ConcatTuple<Cast<R, string[]>>
      : ""}`;

export declare function interpret<
  TContext = DefaultContext,
  TStateSchema extends StateSchema = any,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = {
    value: any;
    context: TContext;
  },
  TResolvedTypesMeta = TypegenDisabled
>(
  machine: AreAllImplementationsAssumedToBeProvided<TResolvedTypesMeta> extends true
    ? StateMachine<
        TContext,
        TStateSchema,
        TEvent,
        TTypestate,
        any,
        any,
        TResolvedTypesMeta
      >
    : `Missing: ${ConcatTuple<
        Cast<
          TuplifyUnion<
            Cast<
              Values<
                Prop<
                  Prop<TResolvedTypesMeta, "resolved">,
                  "missingImplementations"
                >
              >,
              string
            >
          >,
          string[]
        >
      >}`,
  options?: InterpreterOptions
): `Missing: ${Cast<
  Values<Prop<Prop<TResolvedTypesMeta, "resolved">, "missingImplementations">>,
  string
>}`;

This type requires more work but it's already almost functional.

johtso commented 2 years ago

Wow that was quick! I'm equal parts scared and in awe, you know you're getting serious when the SO answers have massive disclaimers 😂

The solution that only tells you one of the missing implementations would totally be good enough for most cases.. it's not unusual that the next type error only be revealed by fixing the last one. But obviously if we can say it all up-front with some magic then all the better!

johtso commented 2 years ago

Just bumped into this again today, but I found a workaround.

If you know that you have a stale reference to an action somewhere in your machine but have no idea what it is you can get a more helpful typescript error by temporarily passing an empty actions object when interpreting the machine:

useInterpret(mainMachine, { actions: {} })
mattpocock commented 2 years ago

@johtso It'll also tell you if you have an action missing, too - so it does guide you step-by-step to find the answer.

  1. You haven't passed enough arguments
  2. You haven't passed actions
  3. You haven't passed myMissingAction
johtso commented 2 years ago

Yep! It definitely leaves you breadcrumbs if you know to follow them! I guess the awkwardness derives from the (necessary?) assumption that if you reference something that doesn't exist, rather than that being a mistake, it's expected that you will pass it in as an option when invoking.

I'm currently not passing in anything when invoking so it would be a much friendlier dev experience to see the errors in the machine itself. Not sure how those things could be reconciled though.. explicit typing of which implementations are expected to be fed in?

mattpocock commented 2 years ago

@johtso You can get those errors at the moment the machine is created by doing machine.withConfig(), which will error if you don't pass everything in.

johtso commented 2 years ago

Ah, that's a good tip. I might just start sticking .withConfig({ actions: {}, guards: {}, services: {} }) on my machine definitions.

sunny-mittal commented 1 year ago

The easiest way I've found to figure out what's missing is just like at the typegen at "missing implementations". It'd be nice to have it inline but this is quick and easy too.