Skip to content

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

Open
@le0-0

Description

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions