As designed in your example, Rectangle is a member of Shape union.
The easily understandable situation is for the computeArea function, which can handle any Shape, hence calling it with a Rectangle is fine.
But, counter-intuitively, it is the opposite for foobar function, which expects a callback handling a Shape, but calling it with a callback that takes a Rectangle gives an error...
As we say, callback arguments are contravariant, i.e. here should foobar expect a callback handling a Rectangle, we could have done foobar(computeArea).
It may be easier to understand why it does not work in the question example, if we imagine that foobar internally builds an arbitrary Shape (which could then be a Triangle), and tries to process it with the given callback: fb would not be appropriate in that case (because it cannot handle the Triangle, only a Rectangle).
Whereas if foobar said it needed a callback handling a Rectangle, it would have meant that internally it would execute that callback only with a Rectangle argument. Hence a callback that can handle any Shape (therefore including a Rectangle), like computeArea, would have been perfectly fine.
In your reproduction code, if I understand correctly, the foobar function is actually:
function getShapeArea(shape: Shape, computeArea: (shape: Shape) => number) {
const area = computeArea(shape);
return area;
}
And TypeScript gives an error when we try to call it with:
getShapeArea(circle, computeCircleArea);
...where circle is a Circle (also a member of Shape union), and computeCircleArea a function that takes a Circle and returns a number.
Here the transpiled JavaScript code runs fine at runtime (despite the TS error message).
But if the callback argument was covariant, we could have also used the function as:
getShapeArea(triangle, computeCircleArea);
...in which case the code would have very probably thrown an Exception (the triangle having no radius).
If we had a computeArea callback that worked for any Shape, then it would work (illustrating the contravariance of the callback argument), because whatever the actual shape 1st argument of getShapeArea function, it could be processed by that hypothetical computeArea callback.
In your precise case, you might want to give TS more hints that the 2 arguments of getShapeArea are related (the callback just need to handle the same shape, not an arbitrary shape). Typically with generics:
function getShapeArea<S extends Shape>(shape: S, computeArea: (shape: S) => number) {
const area = computeArea(shape);
return area;
}
With this, getShapeArea(circle, computeCircleArea) is no longer a TS error!
And getShapeArea(triangle, computeCircleArea) is more obviously a mistake (because computeCircleArea cannot handle the triangle).
Depending on your exact situation, you may even further improve getShapeArea function by automatically detecting the shape and using the appropriate compute function accordingly (without having to specify it everytime as a 2nd argument), using type narrowing:
function getShapeArea(shape: Shape) {
// Using the "in" operator narrowing
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing
if ("radius" in shape) {
// If "Circle" type is the only "Shape" type with a "radius" property,
// then TS automatically guesses that "shape" is a Circle
return computeCircleArea(shape); // Okay because TS now knows that "shape" is a Circle
} else if ("width" in shape && "length" in shape) {
// Similarly, if "Rectangle" is the only "Shape" with both "width" and "length",
// then TS guesses that shape is a Rectangle
return computeRectangleArea(shape); // Okay because TS now knows that "shape" is a Rectangle
}
}