Nino Filiu
Nino Filiu

Reputation: 18493

Key can't be used to index type when it should

I've got the following TS code

type Fruit = { kind: "apple" } | { kind: "grape"; color: "green" | "black" };
type FruitTaste<TFruit extends Fruit> = TFruit["kind"] extends "apple"
  ? "good"
  : TFruit["color"] extends "green"
  ? "good"
  : "bad";

Playground link

It errors at TFruit["color"] with

Type '"color"' cannot be used to index type 'TFruit'.

but it shouldn't, because we're in the ternary side where TFruit should only be restricted to { kind: "grape"; color: "green" | "black" }, and the color key should exist.

Weirdly enough TS doesn't have to have any issue with the "runtime" version of it:

type Fruit = { kind: "apple" } | { kind: "grape"; color: "green" | "black" };
const fruitTaste = (fruit: Fruit) =>
  fruit.kind === "apple" ? "good" : fruit.color === "green" ? "good" : "bad";

Playground link

Why is that? How can I implement the FruitTaste type?

Upvotes: 2

Views: 95

Answers (3)

Nino Filiu
Nino Filiu

Reputation: 18493

Another workaround, thanks to Matt Pocock on Twitter:

TFruit["color"] extends "green"

does not properly disambiguate the union, but

TFruit extends { color: 'green' }

does, and this compiles properly

type Fruit = { kind: "apple" } | { kind: "grape"; color: "green" | "black" };
type FruitTaste<TFruit extends Fruit> = TFruit["kind"] extends "apple"
  ? "good"
  : TFruit extends { color: 'green' }
  ? "good"
  : "bad";

Playground link

Upvotes: 0

Tobias S.
Tobias S.

Reputation: 23825

To the "why" question: TypeScript does not discriminante unions in generic conditionals based on specific properties. TFruit can not be indexed because at the point of this check, TFruit is still a union with two elements of which one does not have a color property.

My workaround would look like this:

type FruitTaste<TFruit extends Fruit> = TFruit extends { kind: "apple" }
  ? "good"
  : (TFruit & { color: string })["color"] extends "green"
    ? "good"
    : "bad";

Before accessing color, we can use an intersection to remind TypeScript that this property should exist.

Playground

Upvotes: 1

SnowSuno
SnowSuno

Reputation: 17

I think you can implement it by seperating the type { kind: "apple" } and { kind: "grape"; color: "green" | "black" }.

type Apple = { kind: "apple" };
type Grape = { kind: "grape"; color: "green" | "black" };
type Fruit = Apple | Grape;
type FruitTaste<TFruit extends Fruit> = TFruit extends Grape
    ? TFruit["color"] extends "green"
        ? "good"
        : "bad"
    : "good";

Upvotes: 1

Related Questions