Craig Otis
Craig Otis

Reputation: 32054

Enforcing type hierarchy in TypeScript even when types are "same enough"

TypeScript uses "structural subtyping" to determine if two types are compatible, which allows me to do a surprising thing like:

abstract class Food {
}
class Vegetable extends Food {
}
class Fruit extends Food {
    // doFruitThings() {}
}

class FruitBowl {
    addFruit(fruit: Fruit) {}
}

new FruitBowl().addFruit(new Vegetable()); // Whaaaaaat?!

TypeScript Playground Link

Note that the above snippet doesn't emit a compiler error until you uncomment the doFruitThings() method and the types become structurally incompatible.

But - I would prefer that the compiler detects that my Vegetable is not a Fruit, even though they currently share the same properties/functions. I'm in the early stages of designing my API and am using types to flesh it out before providing implementation, and am constantly being tripped up by types that "suddenly" become incompatible even though, in my mind, they were not compatible to begin with.

Question: Is there any way to configure tsc to emit a compiler error in the above playground?

Upvotes: 2

Views: 71

Answers (1)

leonardfactory
leonardfactory

Reputation: 3501

Sure, you can use different techniques, even if there isn't an "official" way. For example, with class branding:

abstract class Food { }

class Vegetable extends Food {
    _vegetableBrand!: string;
}
class Fruit extends Food {
    _fruitBrand!: string;
    // doFruitThings() {}
}
class FruitBowl {
    addFruit(fruit: Fruit) {}
}

// error!
new FruitBowl().addFruit(new Vegetable());

This method is even used by TS team. It doesn't throw at runtime, but allows you to exclude structural compatibility. brand suffix is from TS team guidelines.

Playground Link

There are other ways, you can read more here, however in this case interface branding seems the best hypothesis.

Upvotes: 1

Related Questions