TS2339: Array.find in discriminated union not inferring type correctly

In this example I have a cart, which can be filled with different kinds of items, in this case its golfballs and golfclubs, which have their own options.

Typescript Playground link

I get the following error with the code below:

TS2339: Property 'color' does not exist on type '{ color: "blue" | "red" | "white"; } | { variant: "wedge" | "putter"; }'.
  Property 'color' does not exist on type '{ variant: "wedge" | "putter"; }'.
type ProductGolfBall = {
  type: "golfball";
  options: {
    color: "blue" | "red" | "white";
  };
};

type ProductGolfClub = {
  type: "golfclub";
  options: {
    variant: "wedge" | "putter";
  };
};

type CartItem = ProductGolfBall | ProductGolfClub;
type CartItems = Array<CartItem>;

const cart: CartItems = [
  {
    type: "golfball",
    options: {
      color: "blue"
    }
  },
  {
    type: "golfclub",
    options: {
      variant: "wedge"
    }
  },
  {
    type: "golfclub",
    options: {
      variant: "putter"
    }
  }
];

const golfball = cart.find((item) => item.type === "golfball");

if (golfball) { // Check that it's truthy
  // According to typescript this can still be either ProductGolfClub or ProductGolfBall
  console.log(golfball.type)
  console.log(golfball.options.color) // Produces the TS2339 error above
}

Now I just don't see why the golfball variable can still be ProductGolfClub when the find operation only returns true for an array item where the type property is golfball.

I could just cast set the golfball variable as ProductGolfBall, but there must be some other way to have typescript understand what type the variable has.

Upvotes: 5

Views: 1333

Answers (1)

Maciej Sikora
Maciej Sikora

Reputation: 20132

Unfortunetly TS is not able to make type guard from this case out of box. But you can make it manually by saying explicitly what the function will ensure, consider:

function isGolfBall(item: CartItem): item is ProductGolfBall {
    return item.type === "golfball";
}

const golfball = cart.find(isGolfBall)

if (golfball) { // Check that it's truthy
  console.log(golfball.type)
  console.log(golfball.options.color) // no error 👌
}

Most important is :item is ProductGolfBall it means that we explicitly say this function will be a type guard which will pass (return true) only for ProductGolfBall.

Inline solution will do also:

const golfball = cart.find((item): item is ProductGolfBall => item.type === "golfball")

Upvotes: 7

Related Questions