When looking at your provided example, we see that Rectangle
is considered part of the Shape
union.
The interesting scenario arises with the computeArea
function, as it can work with any shape, including a Rectangle
.
However, it becomes counterintuitive when dealing with the foobar
function. This function expects a callback that handles a Shape
, but using a callback designed for a Rectangle
results in an error...
This situation highlights the concept of callback arguments being contravariant. In this case, if foobar
needed a callback to handle a Rectangle
, calling it with computeArea
would have been appropriate.
To better grasp why the example does not function as expected, picture foobar
constructing an arbitrary Shape
internally (potentially a
Triangle</code), and trying to process it with the given callback - which may not be suitable due to its inability to handle a <code>Triangle
.
If foobar
explicitly required a callback for a Rectangle
, then it would only execute that callback with a Rectangle
argument. Therefore, a callback capable of handling any Shape
(including a Rectangle
) like computeArea
would have worked perfectly fine.
In the context of your code snippet, it appears that the foobar
function is essentially:
function getShapeArea(shape: Shape, computeArea: (shape: Shape) => number) {
const area = computeArea(shape);
return area;
}
During attempts to call this function using:
getShapeArea(circle, computeCircleArea);
...where circle
represents a Circle
(also within the Shape
union), and computeCircleArea
is a function expecting a Circle
and returning a number
, TypeScript issues an error.
Despite the TypeScript error message, the transpiled JavaScript code runs without errors during execution.
If the callback argument were covariant, attempting to use the function with:
getShapeArea(triangle, computeCircleArea);
...would most likely result in an Exception (since triangles do not have a radius).
A universal computeArea
callback capable of working with any Shape
illustrates contravariance of the callback argument. It enables processing of any actual shape passed as the first argument of the getShapeArea
function with that hypothetical computeArea
callback.
To address your specific scenario, providing TypeScript with additional hints regarding the relationship between the two arguments of getShapeArea
may prove beneficial. Consider incorporating generics:
function getShapeArea<S extends Shape>(shape: S, computeArea: (shape: S) => number) {
const area = computeArea(shape);
return area;
}
By implementing this approach, utilizing
getShapeArea(circle, computeCircleArea)
no longer triggers a TS error!
Furthermore,
getShapeArea(triangle, computeCircleArea)
clearly indicates an incorrect usage (as
computeCircleArea
cannot handle the triangle).
Depending on the specifics of your situation, enhancing the getShapeArea
function to automatically detect the shape and utilize the corresponding compute function (without requiring explicit specification each time as a second argument) using type narrowing could offer improvements:
function getShapeArea(shape: Shape) {
// Utilizing the "in" operator narrowing
if ("radius" in shape) {
return computeCircleArea(shape);
} else if ("width" in shape && "length" in shape) {
return computeRectangleArea(shape);
}
}