Home > OS >  Typescript: argument of type a is not assignable to parameter of type b
Typescript: argument of type a is not assignable to parameter of type b

Time:08-03

I have interface defined as below:

type Shape = | Triangle | Rectangle;
interface Triangle {...}
interface Rectangle {...}

function foobar(func: (shape: Shape) => void) {...}

function test() {
    const fb = (rect: Rectangle) => {...};
    foobar(fb); // giving me error: argument of type a is not assignable to parameter of type b...
}

but if I do something like below, it is working fine:

function computeArea(shape: Shape) {
  .....
}


const triangle: Triangle = {...};
const rect: Rectangle = {...}

computeArea(triangle);

I am wondering why type Rectangle is compatible with Shape in method computeArea, whereas it is not compatible with Shape in method test. And what's best practice to solve the issue in method test?

I added the mini repro code here: https://playcode.io/934375/

CodePudding user response:

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 a generic:

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
  }
}
  • Related