Skip to content

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

@le0-0

Description

@le0-0

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions