Trick for deriving co/contra-variance

I can never remember if function arguments are covariant or contravariant but I know a trick for how to derive it. The trick is pretty simple and follows from reasoning about the following function (everything that follows is valid TypeScript)

Now lets think about what happens in terms of subtypes and supertypes when we vary the arguments of apply.

If the first argument f expects something of type A can we pass it a supertype of A? Concretely lets suppose A has two fields, one and two, and the supertype U of A has one field, one. Can we safely pass U to f? The answer is no because f can try to use two but it won’t be available. In other words, we can’t replace f with something that expects more from its arguments but we can replace it with something that expects less. More concretely

So as long as A has everything we expect from U, i.e. A extends U, we can safely pass it to f. Now lets think about the return type B.

From the perspective of someone that is calling apply they expect to get back something of type B so as long as f gives back something that conforms to B we are fine. That is another way of saying we can return anything that extends B. More concretely

So we started with something of type A => B and we ended up with something of type U => V where A extends U and V extends B. Abusing notation for a second we have the following relationship

if A extends U and V extends B then (U => V) extends (A => B)

Notice how the relationship between the argument types is reversed and the relationship between return types is not. This is why we say function arguments are contravariant and return types are covariant. If you think of => as a type constructor then it is contravariant in the argument types and covariant in the return types but I can never remember that so I always derive it by starting from apply and then reasoning my way to the end result.