Interfaces vs Classes in TypeScript

In TypeScript, both interfaces and classes are used to define the structure of an object or blueprint for objects, but they serve different purposes and have different behaviors. Understanding when to use an interface and when to use a class is essential to writing clean, maintainable, and efficient TypeScript code. While they might look similar at first glance, one is a "virtual" contract used during development, while the other is a concrete "factory" for creating objects at runtime.

Developer Tip: Think of an Interface as a checklist of requirements and a Class as the actual factory that builds the item according to those requirements.

 

Key Differences Between Interfaces and Classes

Definition of Structure vs Implementation

  • Interface: Defines a contract or structure for objects without providing any implementation details. It defines the shape or blueprint of an object, ensuring that the object adheres to specific rules (properties and methods). It tells TypeScript: "Any object that calls itself a 'User' must have these specific fields."
  • Class: Provides both the structure and the implementation. It defines the blueprint for creating objects and can include implementation of methods, as well as data members (properties). It doesn't just describe the object; it provides the logic for how that object behaves.
Best Practice: Use Interfaces to define the shape of data (like API responses) and Classes when you need to encapsulate logic or state.

Instantiating

  • Interface: Cannot be instantiated. An interface only defines the structure but does not create objects. Since interfaces are removed during the compilation process, they don't exist in the final JavaScript code.
  • Class: Can be instantiated to create objects using the new keyword. A class defines both the structure and the implementation, and you can create instances (objects) of a class that persist at runtime.
Watch Out: Because interfaces are "erased" during compilation, you cannot use instanceof with an interface at runtime. If you need to check types at runtime, use a class.

Method Implementation

  • Interface: Only defines method signatures (the name, parameters, and return type) but does not provide an implementation. Methods defined in an interface need to be implemented by the class that implements the interface.
  • Class: Can define methods with full implementation. Classes can have both abstract methods (unimplemented, meant for subclasses) and concrete methods (fully functional logic).

Inheritance

  • Interface: Can extend multiple interfaces, which allows a type to inherit from several sources. This is powerful for creating complex, reusable data shapes. Interfaces are purely structural and are used for type checking.
  • Class: Can extend only one other class (single inheritance) to inherit both its properties and methods. However, a class can implement multiple interfaces at the same time.
Common Mistake: Trying to "extend" multiple classes. TypeScript only allows a class to inherit from one parent class. If you need a blueprint from multiple sources, use interfaces instead.

Support for Properties and Methods

  • Interface: Defines only the shape of properties and methods. It doesn't contain any implementation logic. It also cannot use access modifiers like private or protected on properties (everything is implicitly public).
  • Class: Contains both the definition (properties) and implementation (methods) of an object. A class can also have constructors, which define how the object is created, and use access modifiers to hide data.

Use Case

  • Interface: Primarily used to define types, especially when you need to ensure that an object conforms to a certain shape or structure, or when working with types that will be implemented by different classes. They are "zero-cost" because they don't add size to your final JS file.
  • Class: Used to define both the structure and the behavior of objects. Classes are used when you need to create instances, manage internal state, or provide method logic for objects that will be reused throughout your app.

 

Example Comparison

Interface Example

interface Person {
  name: string;
  age: number;
  greet(): void;
}

class Employee implements Person {
  constructor(public name: string, public age: number) {}

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

const employee = new Employee("John", 30);
employee.greet();  // Output: Hello, my name is John and I am 30 years old.
  • The Person interface defines the structure for objects, including properties (name, age) and a method (greet()).
  • The Employee class implements the Person interface, ensuring that it provides the correct structure and method implementation. This creates a "safety net" for the developer.

Class Example

class Car {
  constructor(public brand: string, public model: string) {}

  drive() {
    console.log(`The ${this.brand} ${this.model} is driving.`);
  }
}

const car = new Car("Toyota", "Corolla");
car.drive();  // Output: The Toyota Corolla is driving.
  • The Car class not only defines properties (brand, model) but also provides the implementation of the drive() method.
  • An object of type Car can be instantiated, and the method drive() can be invoked directly because the logic is bundled with the definition.
Developer Tip: Use a class when you want to use the constructor to initialize complex values or set default properties automatically when an object is created.

When to Use an Interface

  • Type-checking: If you want to enforce that an object or class conforms to a specific shape or structure (like the JSON response from a REST API), you should use an interface.
  • Multiple implementations: When different classes may have different implementations (e.g., a PdfReport and an ExcelReport) but should both adhere to the same Report structure.
  • Avoiding implementation: If you only need to define the shape of an object without needing the actual logic, interfaces are more suitable and keep your code lightweight.

Example: Interface for Multiple Implementations

interface Animal {
  sound(): void;
}

class Dog implements Animal {
  sound() {
    console.log("Woof!");
  }
}

class Cat implements Animal {
  sound() {
    console.log("Meow!");
  }
}

const dog = new Dog();
dog.sound();  // Output: Woof!

const cat = new Cat();
cat.sound();  // Output: Meow!

Here, both Dog and Cat classes implement the same Animal interface, ensuring that both classes provide a sound() method, but allowing each to decide what that sound actually is.

When to Use a Class

  • Instantiating objects: When you need to create multiple instances of an object with their own internal state.
  • Object creation and behavior: If you need an object that has both data (properties) and functionality (methods) that manipulates that data.
  • Inheritance: When you want to share common logic between related objects. For example, a BaseService class that handles API errors, which is then extended by a UserService.

Example: Class with Inheritance

class Animal {
  constructor(public name: string) {}

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog("Buddy");
dog.speak();  // Output: Buddy barks

Here, the Dog class extends the Animal class and overrides the speak() method. It inherits the name property and the constructor from the parent, saving us from writing repetitive code.

 

Summary

  • Interfaces are used to define structures and types. They disappear after compilation and are best for ensuring that an object or class adheres to a certain contract or shape without adding runtime overhead.
  • Classes provide both structure and behavior. They remain in the compiled JavaScript code, can be instantiated, have constructors, and can be extended through inheritance.

When designing your application, use interfaces when you want to define types and structures for data, and use classes when you need to implement object behavior, create instances, and manage application logic.

Best Practice: When in doubt, start with an Interface. If you later realize you need to add methods with logic or a constructor to initialize data, you can easily convert it into a Class.