2

I'm learning TypeScript about extending the interface. I unintentionally recognized that TypeScript allows extending interface from multiple classes. That made me surprised and I have researched a lot to find more information about but until now, I have not been able to answer questions.

I am wondering that: although TypeScript prevents class inheriting from multi-classes (class has 2 or more direct base classes), why does it permit one interface to extend directly from multiple classes like that code below:


    class Student{
        name:string = ''
    }
    
    class Employee {
        salary:number =0
    }
    
    interface Work_Study extends Student,Employee{
        description:string;
    }
    
17
  • 3
    "one interface that extends directly from multiple classes like in the code below" - it doesn't. The interface extends multiple types - it doesn't care how those types were defined. They might be declared with class, interface, or type. The interface does not care (or know) about the definitions (implementations) of the class methods. Commented Aug 12, 2024 at 9:15
  • 2
    "Is it often used as an alternative to class multiple inheritance" - no. Notice that your Work_Study_Student is neither a subclass of Student nor a subclass of Employee - it does not inherit their methods, and instanceof will return false. All it does is to declare that it is compatible with some types. Commented Aug 12, 2024 at 9:16
  • 2
    Maybe you want to edit to something like "what does it mean when an interface extends a class in TypeScript?". The answer looks like "class X {} introduces two things named X into scope. One is the JS class constructor value X that exists at runtime, and the other is the class instance interface type X that exists only in TS's type system. The latter is what you are referring to with interface Y extends X {}. Thus you can freely extend multiple class instance types, since they are just interfaces. You are not touching the classes at runtime. Does that make it clear? Commented Aug 13, 2024 at 3:33
  • 2
    I mean that if you write declare class X {y: string; z: number} it behaves very much like interface X { y: string; z: number }; declare const X: new () => X;. A class declaration declares an interface-like type corresponding to the class instance type, and a const-like value corresponding to the constructor. The instance type isn't literally an interface, but it behaves almost exactly like one. And when you write interface W extends X {} that X is the type, not the value. Does that make it clear? I'm happy to write an answer but I want to make sure it applies first. Commented Aug 13, 2024 at 3:55
  • 1
    @jcalz Thank you for your explanation, it is really awesome, Could you write the answer? Commented Aug 13, 2024 at 13:18

3 Answers 3

3

When you write a class declaration in TypeScript like

class Student {
    name: string = ''
}

it brings into scopes two things named Student. One is the class constructor value that exists at runtime in JavaScript. It's an object named Student and you can write new Student() to construct a new class instance value. The other is the class instance type that exists only in TypeScript's type system. It's a type named Student and you can write let student: Student to tell TypeScript that the type of student is the instance type of the Student class. Even though they share a name and are related, they are not the same thing.

TypeScript doesn't get confused about which thing you're referring to because values and types live in different syntactic contexts in TypeScript. If you write x = new X(), that X is definitely a value, not a type. If you write let x: X;, that X is definitely a type, not a value. If you write interface Z extends X {}, that X is definitely a type, not a value.

See the TypeScript handbook documentation for more details.


It's also important to note that the class instance type behaves exactly like an interface. It's not declared using the interface keyword, but it behaves the same. Indeed, the Student declaration above is quite similar to the pair

interface Student {
  name: string
}

const Student: new () => Student = class {
  name: string = ""
};

where the two things named Student are defined explicitly and separately.


If we put those together, we can understand what

class Student {
  name: string = ''
}

class Employee {
  salary: number = 0
}

interface Work_Study extends Student, Employee {
  description: string;
}

is doing. The interface declaration with the extends clause is just declaring a new interface which inherits from the Student and Employee types. It has nothing whatsoever to do with the Student and Employee constructors. It's exactly the same as

interface Student {
  name: string
}

interface Employee {
  salary: number;
}

interface Work_Study extends Student, Employee {
  description: string;
}

And multiple inheritance of interfaces is unproblematic.


On the other hand, something like

class Work_Study extends Student, Employee {
  description: string = "oops"
}

is different, in that it's a class declaration and involves the value space. It's invalid because that's the JavaScript extends clause which only allows extending a single parent class. Multiple inheritance in JavaScript classes is prohibited (to avoid the diamond problem where it's ambiguous which implementation to use).

While class Work_Study extends Student, Employee {} and interface Work_Study extends Student, Employee {} look similar, they are in completely different syntactic contexts, and behave differently. The former is illegal multiple class inheritance, while the latter is legal multiple interface inheritance.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

1

If the TypeScript supported multiple inheritance of classes, the diamond problem would be possible.

class A {
    f() {
        // Some implementation
    }
}

class B extends A {
    f() {
        // Overridden implementation of A.f()
    }
}

class C extends A {
    f() {
        // Overridden implementation of A.f()
    }
}

class D extends B, C {
    // Do not override the implementation of f()
}

let d: D = new D();
d.f(); // It is not clear which function to call (from A, B, or C).

In the case of interface inheritance, this problem does not arise.

class A {
    f() {
        // Some implementation
    }
}

class B {
    f() {
        // Some implementation
    }
}

interface C extends A, B {

}

// You CANNOT instantiate an interface!
// let c:C = new C();

// You must create an implementation of the С
class CImpl implements C {
    f(): void {
        // And you are required to implement f().
    }
}

let c:C = new CImpl();
// In this case, there is no ambiguity as in the case of class inheritance.
c.f();

Comments

0

In general, using inheritance is often considered a bad practice. There are several good practices that you need to remember:

  • Interface segregation principle (ISP): "No code should be forced to depend on methods it does not use".

  • Composition reuse principle (CRP): "Classes should favor polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) over inheritance from a base or parent class."

You are not allowed to use multiple inheritance to avoid the diamond problem and to encourage developers to adhere to the CRP principle. This is a design decision by the ECMAScript committee about inheritance in JavaScript.

About your question 1. You are allowed to extend multiple interfaces to comply with the ISP. For example:

interface Disposable {
  dispose: () => void;
}

interface Serializable {
  serialize: () => string;
}

class Student implements Disposable, Serializable {
    name:string = '';
    somethingElse: number = 0;
    serialize() {
      return JSON.stringify(this);
    }
    dispose() {
      // dispose logic
    }
}

If you were allowed to only implement one interface, you would encourage developers to break the ISP principle.

Using multiple interfaces means that when you implement other parts of your application, these parts don't need to be coupled with Student; they can be coupled only with the methods that they need:

function cleanUp(something: Disposable) {
  something.dispose();
}

function serialzie(something: Serializable) {
  return something.serialize();
}

In relation to your questions 2 and 3:

  1. Yes, implementing multiple interfaces is common. Here is an example in the TypeScript code itself.

  2. Yes, same as 2. There is only one limitation, the diamond problem:

interface Student {
    name:string;
    somethingElse: number;
}

interface Employee {
    salary:number;
    somethingElse: string;
}

// 'WorkStudy' cannot simultaneously extend types 'Student' and 'Employee'.
// Named property 'somethingElse' of types 'Student' and 'Employee' are not identical.
interface WorkStudy extends Student, Employee{
    description:string;
}

If you want to reuse code, your only option will be to use composition over inheritance:

interface IStudent {
    name:string;
}

interface IEmployee {
    salary:number;
}

class Student implements IStudent {
  constructor(public name: string) {}
}

class Employee implements IEmployee {
  constructor(public salary: number) {}
}

class WorkStudy implements IStudent, IEmployee{
  private _student: IStudent;
  private _employee: IEmployee;
  constructor(
    public description: string,
    student: IStudent,
    employee: IEmployee
  ) {
    this._employee = employee;
    this._student = student;
  }
  get salary() {
    return this._employee.salary;
  }
  get name() {
    return this._student.name;
  }
}

So, your only option is to adhere to the CRP.

4 Comments

"This is a design decision by the TypeScript team" - uh, no? Why do you write that?
When you are implementing a programming language, it is up to you as the designer to allow or disallow multiple inheritance. In this case, they choose not to support it. JS classes have a single inheritance but you need to remember that TypeScript was implemented long before JavaScript had classes. Disallowing multiple inheritance is always the most likely case because there are documented issues about the oppsosite.
TypeScript was based on the ES6 class proposals, it wasn't implemented before them. JS always had single inheritance, and it was never meant to gain multiple inheritance, so TypeScript designers were pretty much limited by that; it was not their decision.
Fair enough, I changed to the ECMAScript committee. The point I was trying to make is that having multiple or single inheritances has no "reason" other than a choice by the designers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.