What is the difference between inheritance and union types to express polymorphism?

Viewed 348

I am playing with union types in Typescript a I just realised that they can be used to express polymorphism instead of inheritance. Comming from Java I would write something like this.

interface Smiley {
  print(): string;
}

class Happy implements Smiley {
  print() {
    return ':-)';
  }
}

class Sad implements Smiley {
  print() {
    return ':-(';
  }
}

On the other hand, I can express the same thing with unions

class Happy {
  print() {
    return ':-)';
  }
}

class Sad {
  print() {
    return ':-(';
  }
}

type Smiley = Happy | Sad;

What are theoretical or practical difference between these approaches?

One that comes to mind is that the first one allows the value of type Smiley to be anything that implements the Smiley interface. The second one only permits a Smiley to ever be Happy or Sad, nothing else.

Is there anything else that I am missing?

1 Answers

With those specific properties, there isn't much difference. Smiley will behave like the type { print: () => string }. Neither version will force it so that Happy and Sad are the only things matching the type; someone can always write a new object with a print function that returns a string, and it will match Smiley

But as soon as you start adding more properties, they will not behave the same. If you add a common property to both classes, then example 2 will automatically include that property in Smiley. But in example 1, Smiley will only get an extra property if you add it to Smiley, not if you add it to Happy/Sad.

Or more importantly is the case where they have some properties which are different. That's where unions are most useful. Any properties that are shared between the types can be accessed without typescript complaining, but properties that are unique can't be accessed unless you do appropriate checks to narrow down the type.

class Happy {
  print() {
    return ':-)';
  }
  greet() {
    return 'hi';
  }
}

class Sad {
  print() {
    return ':-(';
  }
  exterminate() {
    return 'bye';
  }
}

type Smiley = Happy | Sad;

const example: Smiley = /* some object; maybe it's a Happy, maybe it's a Sad, maybe something else matching Smiley*/

example.print(); // Allowed by typescript
if ('greet' in example) {
  example.greet(); // Allowed, because we have narrowed it down to a Happy
}
example.exterminate(); // Not allowed, because I have no code to confirm it's a Sad
Related