Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript thinks Msg type parameter of Dispatchable is covariant, but it should be contravariant #147

Open
le0-0 opened this issue Jan 13, 2025 · 0 comments · May be fixed by #148
Open

Comments

@le0-0
Copy link

le0-0 commented Jan 13, 2025

Let's say that Cat extends Animal – all Cats are Animals, just with even more restrictions on top. A Dispatchable<Animal> should be assignable to Dispatchable<Cat>. Only Cats will be dispatched to Dispatchable<Cat>, and all Cats are Animals, so a Dispatchable<Animal> is suited to process all Cats that is dispatched to it. Cat is a variant of Animal, but Dispatchable<Animal> is a variant of Dispatchable<Cat>, not the other way around. This means that the relationship between Dispatchable and its type parameter Msg is contravariant.

type LeastRestrictedType = {
  field1: "field1";
};
type MiddleRestrictedType = LeastRestrictedType & { field2: "field2" };
type MostRestrictedType = MiddleRestrictedType & { field3: "field3" };

const system = start();
const actor = spawn(system,
  (state: undefined, message: MiddleRestrictedType): undefined => {
    console.log(message.field1, message.field2);
    return state;
  }
);

// This should fail because the actor actually uses restrictions
// for MiddleRestrictedType that are not present on LeastRestrictedType.
const smallerActorRef: Dispatchable<LeastRestrictedType> = actor;
dispatch(smallerActorRef, {
  field1: "field1",
});

// This fails, but shouldn't because all restrictions on MiddleRestrictedType
// are also present on MostRestrictedType.
const biggerActorRef: Dispatchable<MostRestrictedType> = actor;
dispatch(biggerActorRef, {
  field1: "field1",
  field2: "field2",
  field3: "field3",
});

Expected Behavior

  • The TypeScript type system should realize that Dispatchable and its type parameter Msg are contravariant.
  • Trying to assign Dispatchable<Cat> to Dispatchable<Animal> should fail.
  • Trying to assign Dispatchable<Animal> to Dispatchable<Cat> should succeed.

Current Behavior

  • Trying to assign Dispatchable<Cat> to Dispatchable<Animal> succeeds, which leads to the bug illustrated above where the type system allows a message to be passed to a Dispatchable that doesn't have all the restrictions it assumes/needs.
  • Trying to assign Dispatchable<Animal> to Dispatchable<Cat> fails, even though all Dispatchable<Animal> are perfectly able to process all Cats.

Possible Solution

Probably some in/out annotations on type parameters in Dispatchable and associated types, to caress the type system into realizing the relationship between Dispatchable and its type parameter Msg is contravariant.

Context

I have met this roadblock many times. Most recently, I tried to create a testing function that expected a LocalActorRef<A | B>, to which I tried to pass a LocalActorRef<A | B | C>. If it can process A, B, and C, it will have no trouble if it only gets A and B, but the type system doesn't like this.

@le0-0 le0-0 linked a pull request Jan 22, 2025 that will close this issue
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant