Skip to main content

TypeScript Inheritance

TypeScript inheritance enables developers to create new classes based on existing ones, inheriting their properties and methods. This fundamental concept helps you write cleaner, more maintainable code by promoting reuse and reducing duplication. In this article, we'll cover everything from basic class inheritance to advanced patterns like abstract classes and multiple interface inheritance.

Implementing Class Inheritance in TypeScript

Class inheritance in TypeScript allows one class to inherit properties and methods from another, promoting code reuse and cleaner architecture. You can use the extends keyword to create a new class that inherits from an existing class.

class Animal {
move(distance: number) {
console.log(`Animal moved ${distance}m.`);
}
}

class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}

const dog = new Dog();
dog.bark(); // Woof! Woof!
dog.move(10); // Animal moved 10m.

Access Modifiers

Access modifiers control how members of your TypeScript class can be accessed. TypeScript has three main access modifiers: public, private, and protected.

class BankAccount {
public accountName: string;
private balance: number;
protected accountNumber: number;

constructor(accountName: string, balance: number, accountNumber: number) {
this.accountName = accountName;
this.balance = balance;
this.accountNumber = accountNumber;
}

getBalance() {
return this.balance; // Accessible within the class
}
}

const account = new BankAccount('John Doe', 1000, 123456);
console.log(account.accountName); // John Doe
// console.log(account.balance); // Error: Property 'balance' is private and only accessible within class 'BankAccount'.

When you use TypeScript extends, protected members become available to child classes, while private members remain inaccessible. This control helps maintain encapsulation while enabling inheritance.

Using Interfaces for Inheritance in TypeScript

TypeScript interface inheritance works differently from class inheritance but serves an important role in creating modular, reusable code. Interfaces can extend other interfaces, allowing you to build complex type definitions from simpler ones.

interface Printable {
print(): void;
}

interface Scannable extends Printable {
scan(): void;
}

class Document implements Scannable {
print(): void {
console.log("Printing a document.");
}

scan(): void {
console.log("Scanning a document.");
}
}

Interface Implementation

Classes in TypeScript can implement multiple interfaces, providing flexibility in meeting different requirements. This pattern is common in TypeScript applications where you need to ensure objects conform to multiple contracts.

interface Flyable {
fly(): void;
}

interface Swimmable {
swim(): void;
}

class Duck implements Flyable, Swimmable {
fly() {
console.log('Duck flying...');
}

swim() {
console.log('Duck swimming...');
}
}

const ducky = new Duck();
ducky.fly(); // Duck flying...
ducky.swim(); // Duck swimming...

This approach lets you achieve something similar to multiple inheritance while maintaining type safety.

Extending Classes with TypeScript Inheritance

When you extend a class in TypeScript, you create a new class that inherits all the properties and methods from its parent. The child class can then add its own members or modify inherited behavior.

class Vehicle {
type: string;

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

drive(): void {
console.log(`Driving a ${this.type}.`);
}
}

class Car extends Vehicle {
model: string;

constructor(type: string, model: string) {
super(type);
this.model = model;
}

honk(): void {
console.log(`Honking the horn of a ${this.model} ${this.type}.`);
}
}

Overriding Methods

Method overriding lets you provide specific implementations in child classes. When you override a method, you replace the parent class implementation with your own version that better suits the child class.

class Shape {
area(): number {
return 0;
}
}

class Circle extends Shape {
radius: number;

constructor(radius: number) {
super();
this.radius = radius;
}

area(): number {
return Math.PI * (this.radius ** 2);
}
}

Using TypeScript extends provides the foundation for building complex class hierarchies. When working with inheritance in larger applications, consider exploring TypeScript best practices to maintain clean, maintainable code.

Achieving Multiple Inheritance with TypeScript Interfaces

TypeScript doesn't support multiple class inheritance directly, but you can achieve similar functionality through interfaces. By implementing multiple interfaces, a class can fulfill multiple contracts and inherit behavior definitions from various sources.

interface Flyable {
fly(): void;
}

interface Drivable {
drive(): void;
}

class FlyingCar implements Flyable, Drivable {
fly(): void {
console.log("Flying a car.");
}

drive(): void {
console.log("Driving a car.");
}
}

This pattern offers several advantages over traditional multiple inheritance:

  • Avoids the diamond problem common in multiple inheritance
  • Provides clear contracts for implementing classes
  • Allows for flexible composition of behaviors

When working with TypeScript interface patterns, you can create complex object structures while maintaining type safety. For more advanced patterns and techniques, explore the Convex API generation secrets to see how TypeScript's type system can power sophisticated architectures.

Overriding Methods in TypeScript Class Inheritance

Method overriding allows you to redefine inherited methods to suit the specific needs of your child class. When you override a method, you provide a new implementation that replaces the parent class version.

class Printer {
print() {
console.log('Printing document...');
}
}

class ColorPrinter extends Printer {
print() {
console.log('Printing document in color...');
}
}

const colorPrinter = new ColorPrinter();
colorPrinter.print(); // Printing document in color...

In TypeScript 4.3 and later, you can use the override keyword to explicitly mark overridden methods, which helps catch errors if the parent method signature changes:

class Printer {
print(copies: number = 1) {
console.log(`Printing ${copies} copies...`);
}
}

class ColorPrinter extends Printer {
override print(copies: number = 1) {
console.log(`Printing ${copies} color copies...`);
}
}

This explicit approach to TypeScript class inheritance helps prevent bugs and makes your code more maintainable. For a deeper understanding of TypeScript inheritance patterns, check out the comprehensive guide to TypeScript inheritance.

Using TypeScript Inheritance for Code Reuse

TypeScript inheritance enables code reuse and reduces duplication by creating a base class with common properties and methods.

class Person {
name: string;

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

greet(): void {
console.log(`Hello, my name is ${this.name}.`);
}
}

class Employee extends Person {
department: string;

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

work(): void {
console.log(`I am working in the ${this.department} department.`);
}
}

This pattern works well for:

  • Shared validation logic
  • Common data structures
  • Reusable business rules
  • Base functionality that multiple classes need

When using TypeScript inheritance for code reuse, consider how it affects your application's structure. The article on useState-less architecture demonstrates how inheritance patterns can simplify state management in React applications.

Managing Constructors in TypeScript Inheritance

When working with inheritance, constructors require special attention. The child class constructor must call super() before accessing this, ensuring the parent class initializes properly.

class Animal {
name: string;

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

class Dog extends Animal {
breed: string;

constructor(name: string, breed: string) {
super(name); // Must call super before using 'this'
this.breed = breed;
}
}

Key constructor rules:

  • Call super() before using this in a child class
  • Pass required parent parameters to super()
  • Child constructors can have additional parameters
  • If you don't define a constructor, TypeScript creates one that calls super() automatically

When working with constructor patterns, understanding TypeScript best practices helps avoid common pitfalls.

Common Challenges and Solutions

Working with TypeScript inheritance can present several challenges. Here are specific solutions to common problems:

The Diamond Problem

While TypeScript avoids traditional diamond problems through single inheritance, complex interface hierarchies can create similar issues. The solution is to use interface segregation and composition:

// Problem: Multiple interfaces with same method names
interface Printable {
print(): void;
}

interface Drawable {
print(): void; // Conflicts with Printable
}

// Solution: Use interface composition
interface PrintableDraw {
printDocument(): void;
draw(): void;
}

Super Method Calls

Forgetting to call the parent method can break functionality:

class BaseLogger {
log(message: string) {
console.log(`[BASE]: ${message}`);
}
}

class AdvancedLogger extends BaseLogger {
override log(message: string) {
super.log(message); // Don't forget this!
console.log(`[ADVANCED]: ${message}`);
}
}

Constructor Order Issues

TypeScript enforces super() before accessing this, but timing matters:

class Component {
constructor(protected id: string) {}
}

class Button extends Component {
constructor(id: string, public label: string) {
super(id); // Must be first
// Now 'this' is available
}
}

Final Thoughts on TypeScript Inheritance

TypeScript inheritance provides a solid foundation for building structured, maintainable applications. By understanding class inheritance, interface extension, and method overriding, you'll write cleaner code that's easier to scale and maintain.

Remember these key points:

  • Use TypeScript extends for class inheritance when sharing implementation
  • Choose TypeScript interface extension when defining contracts
  • Apply method overriding thoughtfully to customize behavior
  • Leverage inheritance for code reuse, but don't overuse it