Execute Program

Everyday TypeScript: Single and Multiple Inheritance

Welcome to the Single and Multiple Inheritance lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen extends, implements, and some differences between them. There's another critical difference between extends and implements that deserves its own lesson.

  • In JavaScript and TypeScript, classes can only extend one other class. This is called "single inheritance", in contrast to the "multiple inheritance" allowed by some other languages. In C++ or Python, a class can extend many parent classes simultaneously. This makes those languages flexible, but it can also lead to confusing code.

  • Suppose that a class extends two different parent classes. TypeScript doesn't support that, but if it did then the syntax might look like class User extends Authenticatable, DatabaseRow { ... }.

  • But what happens if Authenticatable and DatabaseRow both define an isValid method? The User class can only have one isValid method, so which one does it get? If we don't notice that both classes provide the same method, or if we misunderstand which of them is inherited, then we can get very confusing bugs!

  • We don't have to worry about this problem in TypeScript because it doesn't support multiple inheritance at all. If we try to use the syntax that we imagined above, it's a type error.

  • >
    class Authenticatable {
    }

    class DatabaseRow {
    }

    class User extends Authenticatable, DatabaseRow {
    }
    Result:
    type error: Classes can only extend a single class.Pass Icon
  • However, TypeScript does allow a class to implement multiple types. Fortunately, the problem discussed above doesn't exist in that case.

  • If we implement two types that have the same property, then the class's property must satisfy both types. Often, both implemented types have the same type for that property. Then everything is simple: the class's property must have that same type.

  • >
    interface HasName {
    name: string
    }

    interface Nameable {
    name: string
    }

    class User implements HasName, Nameable {
    name: string;

    constructor(name: string) {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    'Amir'Pass Icon
  • >
    interface HasName {
    name: string
    }

    interface Nameable {
    name: string
    }

    class User implements HasName, Nameable {
    name: number;

    constructor(name: string) {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    type error: Property 'name' in type 'User' is not assignable to the same property in base type 'HasName'.
      Type 'number' is not assignable to type 'string'.Pass Icon
  • Things get more complex when both interfaces define the same property, but give it different types. When that happens, the class's property must satisfy both types simultaneously.

  • In the next examples, User implements two interfaces. One interface requires name to be the literal type 'Amir' | 'Betty', and the other interface requires it to be 'Amir' | 'Cindy'. The only type that satisfies both of those types simultaneously is 'Amir', so the User class's name must have that type.

  • >
    interface NameIsAmirOrBetty {
    name: 'Amir' | 'Betty'
    }

    interface NameIsAmirOrCindy {
    name: 'Amir' | 'Cindy'
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class User implements NameIsAmirOrBetty, NameIsAmirOrCindy {
    name: string;

    constructor(name: string) {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    type error: Property 'name' in type 'User' is not assignable to the same property in base type 'NameIsAmirOrBetty'.
      Type 'string' is not assignable to type '"Amir" | "Betty"'.Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class User implements NameIsAmirOrBetty, NameIsAmirOrCindy {
    name: 'Amir' | 'Betty';

    constructor(name: 'Amir' | 'Betty') {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    type error: Property 'name' in type 'User' is not assignable to the same property in base type 'NameIsAmirOrCindy'.
      Type '"Amir" | "Betty"' is not assignable to type '"Amir" | "Cindy"'.
        Type '"Betty"' is not assignable to type '"Amir" | "Cindy"'.Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class User implements NameIsAmirOrBetty, NameIsAmirOrCindy {
    name: 'Amir';

    constructor(name: 'Amir') {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    'Amir'Pass Icon
  • Let's consider one other scenario. What if we try to implement two types with the same property name, but incompatible property types?

  • The TypeScript designers had multiple options here. They could've made this work like type intersection. If we intersect two incompatible types, we get the never type, leading to error messages like this:

  • >
    type StringAndNumber = string & number;
    const name: StringAndNumber = 'Amir';
    Result:
    type error: Type '"Amir"' is not assignable to type 'never'.Pass Icon
  • Errors involving never tend to be very confusing. The error above happens on the const name line, not the type StringAndNumber line. In a real system, those two lines could live in different files, so it would be time-consuming to find where the never type came from.

  • Fortunately, TypeScript's designers took a different path for implements. Implementing two incompatible interfaces doesn't give us a never type. Instead, it causes an error directly in the class definition.

  • >
    interface NameMustBeString {
    name: string
    }

    interface NameMustBeNumber {
    name: number
    }

    class User implements NameMustBeString, NameMustBeNumber {
    name: string;

    constructor(name: string) {
    this.name = name;
    }
    }

    const amir = new User('Amir');
    amir.name;
    Result:
    type error: Property 'name' in type 'User' is not assignable to the same property in base type 'NameMustBeNumber'.
      Type 'string' is not assignable to type 'number'.Pass Icon
  • Take a moment to read through that error message. It's very specific and clear! It tells us which property is incompatible, which of the implemented interfaces caused the incompatibility, and exactly what the incompatible types were.

  • This lesson only contained good news! TypeScript doesn't support multiple inheritance via extends, so we don't have to worry about the confusing side effects that it would cause. On the other hand, it does support multiple implements on a single class, but the error messages are clear and specific, so debugging is relatively easy.