-1

Can anyone describe the nature of the typing issue that creates a compiler error in the code below, and maybe suggest an alternative approach that satisfies type-safety? I am aware that I can bypass type-safety using any and as, and I don't consider this a solution.

The call to evidence() is central to the problem and is equivalent to the type-constrained calls that I need in the real case.

I believe I am constraining a type correctly, but as soon as I rely on the constraint I get told there might be a different subtype flying around, but I don't understand how any subtype wouldn't meet the constraint I'm relying on, (that "artist" is one of its allowed values).

The constraint extends "artist" is intended to achieve that. I don't use TaleState<"artist"> because it is allowed for the TaleState to include other Roles as well as "artist", but it must have "artist" for the logic of the evidence() call to run.

The error is inlined in the code below, described under PROBLEM and visible in the Typescript Playground.

export type Role = "artist" | "writer" | "maker";

export interface TaleState<TaleRole extends Role> {
  rolesVisited: Record<TaleRole, boolean>;
}

export function evidence<TaleRole extends Role, Evidenced extends TaleRole>(
  state: TaleState<TaleRole>,
  ...roles: [Evidenced, ...Evidenced[]]
) {
  for (const role of roles) {
    state.rolesVisited[role] = true;
  }
}

function artistTale<TaleRole extends "artist" >(
  state: TaleState<TaleRole>
) {
  /** the call below has a compiler error which reads...
   * Argument of type '"artist"' is not assignable to parameter of type 'Evidenced'.
   * '"artist"' is assignable to the constraint of type 'Evidenced', 
   * but 'Evidenced' could be instantiated with a different subtype of constraint '"artist"'
   */
  evidence(state, "artist");
}

PROBLEM

Currently there is a typing issue in the artistTale function. I was expecting to be able to run this function on any TaleState that includes "artist" in its Roles.

The line which reads evidence(state, "artist"); has the following compilation error...

Argument of type '"artist"' is not assignable to parameter of type 'Evidenced'. '"artist"' is assignable to the constraint of type 'Evidenced', but 'Evidenced' could be instantiated with a different subtype of constraint '"artist"'

I'm struggling to see how extends "artist" isn't a suitable constraint here.

I'm sure I've got something fundamentally backwards in my understanding but I'm struggling to see what. Probably I have a blind spot in my reasoning about how extends expresses the typing of literals which is getting me into these tight spots.

I've tried various tricks to control inference so that types are driven by the state values, but probably I'm hitting a different difficulty now.

QUESTION

Can anyone help me understand the way in which this could be a type error?

How should I define artistTale instead - a function that allows me to manipulate a suitably-constrained TaleState (making evidence callbacks that are valid assuming the Tale contains the roles I'm manipulating).

BACKGROUND

This simplified errored example is from a portfolio API. The project demonstrates Roles by telling Tales.

Tale typing constrains roles that a Tale declares it will evidence. An evidence callback that manipulates the state (with autocompletion for roles) can record when a role is evidenced during the Tale.

Having both an Evidenced Role type and also passing around roles runtime values is crucial (e.g. having an actual list associated with a Tale allows checking all roles are eventually evidenced during test execution of the Tale). For this reason, the API is inference-heavy.

This line shows an example directly from the experimental API under development. The approach aims to benefit from terse, declarative-style nested function calls, but which are type-safe. Each Tale declares the roles it evidences, and no call nested inside tale() should attempt to evidence a role not in the list, which is why the inference of Evidenced is so crucial.

Unfortunately, to be able to include the one-shot illuminationsIntro from this line I had to declare it as Beat<any> which is to say, I broadened the type so that it is no longer constrained to only the valid roles for the tale it's nested in, meaning both type AND runtime logic errors can then creep in.

cefn
  • 2,895
  • 19
  • 28

1 Answers1

0

With my approach you lose type-safety but you hide it inside artistTale and outside of it you get the correct type-safe autocompletion

function artistTale<T extends 'artist', E extends T>(
      state: TaleState<T>
    ) {
      evidence(state, 'artist' as E);
    }
  • Thanks for looking at this, Lazar. I know I can remove the compiler error, but the use of `as` and `any` are effectively turning the compiler off, and the aim here is to have type-safety and autocompletion. – cefn Mar 01 '23 at 15:32
  • Maybe you lose type-safety but you hide it inside artistTale and outside you get the correct type-safe autocompletion. – Lazar Todorović Mar 01 '23 at 15:38
  • Fair point - an artistTale call is limited to only a TaleState having an "artist" role. The real case is more complex, and the loss of type safety and inference would be more costly and harder to reason about (calls are nested, so every layer of callbacks would need typing bodges). Preferably authors would rely on patterns and callbacks with bombproof behaviour and compiler feedback but I'm making some kind of fundamental error that I need to unpick. This error is a microcosm of the problem. If solved directly in a type-safe way, should unpick lots of other parts of this API. – cefn Mar 01 '23 at 16:55
  • Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, **can you [edit] your answer to include an explanation of what you're doing** and why you believe it is the best approach? – Jeremy Caney Mar 02 '23 at 00:42
  • @cefn share more of the real case – Lazar Todorović Mar 02 '23 at 01:45
  • Thanks, Lazar and Jeremy. My reason for not accepting Lazar's solution isn't that there's an XY problem and I didn't get a solution for Y. I also didn't get a solution for X. Using `as` to silence the compiler doesn't uncover what's ill-conceived in the typing, what the compiler error means, what should be done instead. I believe the `evidence()` call is equivalent in the real API. My comment is that the cost of bypassing the compiler every time we write a call to `evidence()` is compounded in a real-world case. That's why I want to SOLVE the typing as described in the question, not skip it. – cefn Mar 02 '23 at 09:29
  • I will edit the answer for more context, and consider if there's a different example which can include more of the context, although I have a feeling it will just add more noise. – cefn Mar 02 '23 at 09:31