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.