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 betweenextendsandimplementsthat 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
AuthenticatableandDatabaseRowboth define anisValidmethod? TheUserclass can only have oneisValidmethod, 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.
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'
>
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'.
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,
Userimplements two interfaces. One interface requiresnameto 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 theUserclass'snamemust 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"'.
- 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"'. - 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'
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
nevertype, 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'.
Errors involving
nevertend to be very confusing. The error above happens on theconst nameline, not thetype StringAndNumberline. In a real system, those two lines could live in different files, so it would be time-consuming to find where thenevertype came from.Fortunately, TypeScript's designers took a different path for
implements. Implementing two incompatible interfaces doesn't give us anevertype. 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'.
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 multipleimplementson a single class, but the error messages are clear and specific, so debugging is relatively easy.