Logo

Home

About

Blog

Contact

Buy Merch

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Implementing Interfaces in Classes

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Abstract Classes and Methods
TypeScript
8m
Mar 8, 2026

Abstract Classes and Methods

A comprehensive guide to TypeScript abstract classes covering abstract methods, partial implementation, inheritance patterns, and when to use abstract classes vs interfaces.

#TypeScript Abstract Classes#Abstract Methods+5
Static Members and Utility Classes
TypeScript
6m
Mar 1, 2026

Static Members and Utility Classes

A comprehensive guide to TypeScript static members covering static properties, static methods, utility classes, singleton pattern, and when to use class-level vs instance-level code.

#TypeScript Static#Static Methods+5
Classes in TypeScript: The Basics
TypeScript
7m
Feb 8, 2026

Classes in TypeScript: The Basics

A comprehensive introduction to TypeScript classes covering properties, methods, constructors, and object instantiation with type safety.

#TypeScript Classes#OOP+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • Understanding Interface Implementation
  • The implements Keyword
  • Implementing Multiple Interfaces
  • Structural Typing in Action
  • Interfaces vs Abstract Classes
  • Common Patterns
  • Combining Interfaces and Inheritance
  • Best Practices and Guidelines
TypeScript

Implementing Interfaces in Classes

March 22, 2026•13 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript interface implementation in classes visualization
Implementing Interfaces in Classes

Understanding Interface Implementation

Interfaces define contracts—shapes that objects must conform to. When a class implements an interface, it promises to provide all the properties and methods that interface requires. TypeScript enforces this at compile time.

Think of an interface as a job description. A class that implements it is like an employee who must fulfill all the job requirements. The interface doesn't care how the work gets done, only that it gets done.

interface Printable {
  print(): void;
}
 
class Document implements Printable {
  constructor(private content: string) {}
 
  print(): void {
    console.log(this.content);
  }
}
 
class Image implements Printable {
  constructor(private url: string) {}
 
  print(): void {
    console.log(`[Image: ${this.url}]`);
  }
}
 
function printItem(item: Printable): void {
  item.print();
}
 
const doc = new Document("Hello, world!");
const img = new Image("photo.jpg");
 
printItem(doc); // "Hello, world!"
printItem(img); // "[Image: photo.jpg]"

Both Document and Image implement Printable, so they can be used interchangeably wherever Printable is expected. Each implements print() differently, but both fulfill the contract.

Interfaces Are Contracts

Interfaces define what a class must do, not how to do it. They're compile-time contracts that disappear in JavaScript. They exist purely to help TypeScript catch errors during development.

The implements Keyword

The implements keyword tells TypeScript that a class will fulfill an interface's contract. If the class doesn't provide everything the interface requires, TypeScript throws a compile error.

Basic Implementation

interface Vehicle {
  speed: number;
  accelerate(): void;
  brake(): void;
}
 
class Car implements Vehicle {
  speed: number = 0;
 
  accelerate(): void {
    this.speed += 10;
    console.log(`Accelerating. Speed: ${this.speed} mph`);
  }
 
  brake(): void {
    this.speed = Math.max(0, this.speed - 10);
    console.log(`Braking. Speed: ${this.speed} mph`);
  }
}
 
const car = new Car();
car.accelerate(); // "Accelerating. Speed: 10 mph"
car.brake(); // "Braking. Speed: 0 mph"

Car must provide speed, accelerate(), and brake() because Vehicle requires them.

Missing Members Cause Errors

interface Logger {
  log(message: string): void;
  warn(message: string): void;
  error(message: string): void;
}
 
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
 
  warn(message: string): void {
    console.warn(message);
  }
 
  // error() missing!
  // Error: Class 'ConsoleLogger' incorrectly implements interface 'Logger'.
  // Property 'error' is missing in type 'ConsoleLogger' but required in type 'Logger'.
}
 
// Fix:
class ConsoleLoggerFixed implements Logger {
  log(message: string): void {
    console.log(message);
  }
 
  warn(message: string): void {
    console.warn(message);
  }
 
  error(message: string): void {
    console.error(message);
  }
}

TypeScript won't let you compile until all interface members are implemented.

Type Compatibility

interface User {
  name: string;
  email: string;
}
 
class Admin implements User {
  name: string;
  email: string;
  permissions: string[] = ["admin"];
 
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}
 
// Admin has everything User requires, plus extra
const user: User = new Admin("Alice", "[email protected]");
console.log(user.name); // "Alice"
// console.log(user.permissions); // Error: 'permissions' doesn't exist on type 'User'

Classes can have more than the interface requires. When used as the interface type, only interface members are accessible.

Interface Implementation

Contract fulfillment with implements keyword

Contract
interface Serializable
Required Members
toJSON(): string
fromJSON(json: string): void

Contract: All members must be implemented

Implementation
class User
Class Membersimplements Serializable
name: string
email: string
toJSON(): string
fromJSON(json: string): void

Fulfillment: All interface requirements satisfied

Required: Implements interface member
Additional: Extra class functionality

Implementing Multiple Interfaces

Classes can implement multiple interfaces. This is TypeScript's version of multiple inheritance—you get the contracts of multiple interfaces without implementation conflicts.

Multiple Interface Implementation

interface Saveable {
  save(): void;
}
 
interface Loadable {
  load(): void;
}
 
interface Deletable {
  delete(): void;
}
 
class DataModel implements Saveable, Loadable, Deletable {
  constructor(private id: string) {}
 
  save(): void {
    console.log(`Saving ${this.id}...`);
  }
 
  load(): void {
    console.log(`Loading ${this.id}...`);
  }
 
  delete(): void {
    console.log(`Deleting ${this.id}...`);
  }
}
 
const model = new DataModel("user-123");
model.save();
model.load();
model.delete();

DataModel must implement all methods from all three interfaces.

Mixing Different Concerns

interface Comparable {
  compareTo(other: Comparable): number;
}
 
interface Serializable {
  toJSON(): string;
  fromJSON(json: string): void;
}
 
interface Cloneable {
  clone(): this;
}
 
class Product implements Comparable, Serializable, Cloneable {
  constructor(
    public name: string,
    public price: number
  ) {}
 
  compareTo(other: Product): number {
    return this.price - other.price;
  }
 
  toJSON(): string {
    return JSON.stringify({ name: this.name, price: this.price });
  }
 
  fromJSON(json: string): void {
    const data = JSON.parse(json);
    this.name = data.name;
    this.price = data.price;
  }
 
  clone(): this {
    return new Product(this.name, this.price) as this;
  }
}
 
const p1 = new Product("Laptop", 1200);
const p2 = new Product("Mouse", 25);
 
console.log(p1.compareTo(p2)); // 1175 (laptop is more expensive)
console.log(p1.toJSON()); // '{"name":"Laptop","price":1200}'
 
const p3 = p1.clone();
console.log(p3.name); // "Laptop"

Each interface represents a different capability. The class gains all capabilities by implementing all interfaces.

Interface Conflicts

interface A {
  method(): string;
}
 
interface B {
  method(): number; // Different return type!
}
 
// Error: Interface 'B' cannot simultaneously extend types 'A' and 'B'.
// Named property 'method' of types 'A' and 'B' are not identical.
// class Conflict implements A, B {
//   method(): ??? // Can't satisfy both!
// }

When interfaces have conflicting signatures, you can't implement both. Redesign your interfaces or use composition instead.

Interface Conflicts

If multiple interfaces define the same member with incompatible types, you cannot implement both. This is rare but when it happens, consider whether those interfaces should really be separate concerns.

Structural Typing in Action

TypeScript uses structural typing (duck typing). If an object has the right shape, it satisfies the interface—even without explicitly implementing it.

Implicit Satisfaction

interface Point {
  x: number;
  y: number;
}
 
class Coordinate implements Point {
  constructor(
    public x: number,
    public y: number
  ) {}
}
 
// Doesn't implement Point, but has the right shape
class Position {
  constructor(
    public x: number,
    public y: number,
    public z: number
  ) {}
}
 
function printPoint(p: Point): void {
  console.log(`(${p.x}, ${p.y})`);
}
 
const coord = new Coordinate(10, 20);
const pos = new Position(30, 40, 50);
 
printPoint(coord); // ✅ Explicitly implements Point
printPoint(pos); // ✅ Also works! Has x and y

Position doesn't explicitly implement Point, but it has x and y, so TypeScript considers it compatible.

Object Literals

interface Config {
  host: string;
  port: number;
  secure?: boolean;
}
 
class ServerConfig implements Config {
  constructor(
    public host: string,
    public port: number,
    public secure: boolean = false
  ) {}
}
 
function connect(config: Config): void {
  const protocol = config.secure ? "https" : "http";
  console.log(`Connecting to ${protocol}://${config.host}:${config.port}`);
}
 
// Works with class instances
connect(new ServerConfig("localhost", 3000));
 
// Also works with plain objects
connect({ host: "example.com", port: 443, secure: true });
 
// Also works with objects that have extra properties
connect({ host: "test.com", port: 8080, debug: true } as Config);

Functions accepting interfaces work with any object that has the required shape.

Duck Typing Benefits

interface Renderable {
  render(): string;
}
 
class Button implements Renderable {
  constructor(private label: string) {}
 
  render(): string {
    return `<button>${this.label}</button>`;
  }
}
 
// Doesn't explicitly implement Renderable
class Text {
  constructor(private content: string) {}
 
  render(): string {
    return `<p>${this.content}</p>`;
  }
}
 
function renderComponent(component: Renderable): void {
  console.log(component.render());
}
 
renderComponent(new Button("Click me")); // ✅ Works
renderComponent(new Text("Hello")); // ✅ Also works!

Text never says it implements Renderable, but it has a render() method with the right signature, so it's compatible.

Structural vs Nominal Typing

TypeScript uses structural typing—"if it walks like a duck and quacks like a duck, it's a duck." Languages like Java use nominal typing—"it's only a duck if it explicitly says it's a duck." Structural typing is more flexible but requires careful interface design.

Interfaces vs Abstract Classes

Both define contracts, but they work differently and serve different purposes.

When to Use Interfaces

// ✅ Pure contract, no implementation
interface Validator {
  validate(value: unknown): boolean;
  getErrors(): string[];
}
 
// Multiple implementations, no shared code
class EmailValidator implements Validator {
  private errors: string[] = [];
 
  validate(value: unknown): boolean {
    this.errors = [];
    const email = String(value);
 
    if (!email.includes("@")) {
      this.errors.push("Missing @ symbol");
      return false;
    }
 
    return true;
  }
 
  getErrors(): string[] {
    return this.errors;
  }
}
 
class PasswordValidator implements Validator {
  private errors: string[] = [];
 
  validate(value: unknown): boolean {
    this.errors = [];
    const password = String(value);
 
    if (password.length < 8) {
      this.errors.push("Too short (min 8 characters)");
      return false;
    }
 
    return true;
  }
 
  getErrors(): string[] {
    return this.errors;
  }
}

Interfaces work when implementations are completely different with no shared code.

When to Use Abstract Classes

// ✅ Shared implementation + contract
abstract class Validator {
  protected errors: string[] = [];
 
  abstract validate(value: unknown): boolean;
 
  // Shared implementation
  getErrors(): string[] {
    return [...this.errors];
  }
 
  clearErrors(): void {
    this.errors = [];
  }
 
  isValid(value: unknown): boolean {
    this.clearErrors();
    return this.validate(value);
  }
}
 
class EmailValidatorGood extends Validator {
  validate(value: unknown): boolean {
    const email = String(value);
 
    if (!email.includes("@")) {
      this.errors.push("Missing @ symbol");
      return false;
    }
 
    return true;
  }
}

Abstract classes share common code while enforcing that subclasses implement specific methods.

Combining Both

interface Formatter {
  format(value: string): string;
}
 
abstract class BaseLogger implements Formatter {
  protected logs: string[] = [];
 
  abstract format(value: string): string;
 
  log(message: string): void {
    const formatted = this.format(message);
    this.logs.push(formatted);
    console.log(formatted);
  }
 
  getLogs(): string[] {
    return [...this.logs];
  }
}
 
class JSONLogger extends BaseLogger {
  format(value: string): string {
    return JSON.stringify({ message: value, timestamp: new Date() });
  }
}
 
class PlainLogger extends BaseLogger {
  format(value: string): string {
    return `[${new Date().toISOString()}] ${value}`;
  }
}

The interface defines the format() contract. The abstract class implements Formatter and adds shared logging infrastructure. Subclasses provide their own formatting logic.

Common Patterns

Repository Pattern

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<boolean>;
}
 
interface User {
  id: string;
  name: string;
  email: string;
}
 
class UserRepository implements Repository<User> {
  private users: Map<string, User> = new Map();
 
  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }
 
  async findAll(): Promise<User[]> {
    return Array.from(this.users.values());
  }
 
  async save(user: User): Promise<void> {
    this.users.set(user.id, user);
  }
 
  async delete(id: string): Promise<boolean> {
    return this.users.delete(id);
  }
}
 
class ProductRepository implements Repository<{ id: string; name: string }> {
  private products: Map<string, { id: string; name: string }> = new Map();
 
  async findById(id: string): Promise<{ id: string; name: string } | null> {
    return this.products.get(id) || null;
  }
 
  async findAll(): Promise<{ id: string; name: string }[]> {
    return Array.from(this.products.values());
  }
 
  async save(product: { id: string; name: string }): Promise<void> {
    this.products.set(product.id, product);
  }
 
  async delete(id: string): Promise<boolean> {
    return this.products.delete(id);
  }
}

Strategy Pattern

interface SortStrategy<T> {
  sort(items: T[]): T[];
}
 
class BubbleSort<T> implements SortStrategy<T> {
  sort(items: T[]): T[] {
    const arr = [...items];
    // Bubble sort implementation
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}
 
class QuickSort<T> implements SortStrategy<T> {
  sort(items: T[]): T[] {
    if (items.length <= 1) return items;
 
    const pivot = items[0];
    const left = items.slice(1).filter((x) => x <= pivot);
    const right = items.slice(1).filter((x) => x > pivot);
 
    return [...this.sort(left), pivot, ...this.sort(right)];
  }
}
 
class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
 
  setStrategy(strategy: SortStrategy<T>): void {
    this.strategy = strategy;
  }
 
  sort(items: T[]): T[] {
    return this.strategy.sort(items);
  }
}
 
const numbers = [5, 2, 8, 1, 9];
const sorter = new Sorter(new BubbleSort<number>());
 
console.log(sorter.sort(numbers)); // [1, 2, 5, 8, 9]
 
sorter.setStrategy(new QuickSort<number>());
console.log(sorter.sort(numbers)); // [1, 2, 5, 8, 9]

Observer Pattern

interface Observer {
  update(data: unknown): void;
}
 
interface Subject {
  attach(observer: Observer): void;
  detach(observer: Observer): void;
  notify(): void;
}
 
class NewsPublisher implements Subject {
  private observers: Observer[] = [];
  private latestNews: string = "";
 
  attach(observer: Observer): void {
    this.observers.push(observer);
  }
 
  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
 
  notify(): void {
    this.observers.forEach((observer) => observer.update(this.latestNews));
  }
 
  publishNews(news: string): void {
    this.latestNews = news;
    this.notify();
  }
}
 
class EmailSubscriber implements Observer {
  constructor(private email: string) {}
 
  update(data: unknown): void {
    console.log(`Email to ${this.email}: ${data}`);
  }
}
 
class SMSSubscriber implements Observer {
  constructor(private phone: string) {}
 
  update(data: unknown): void {
    console.log(`SMS to ${this.phone}: ${data}`);
  }
}
 
const publisher = new NewsPublisher();
const subscriber1 = new EmailSubscriber("[email protected]");
const subscriber2 = new SMSSubscriber("555-1234");
 
publisher.attach(subscriber1);
publisher.attach(subscriber2);
 
publisher.publishNews("Breaking: TypeScript 6.0 Released!");
// Output:
// Email to [email protected]: Breaking: TypeScript 6.0 Released!
// SMS to 555-1234: Breaking: TypeScript 6.0 Released!

Combining Interfaces and Inheritance

You can both extend a class and implement interfaces. This gives you inherited functionality plus contract enforcement.

Extending and Implementing

interface Auditable {
  createdAt: Date;
  updatedAt: Date;
  logChange(action: string): void;
}
 
class BaseModel {
  protected id: string;
 
  constructor(id: string) {
    this.id = id;
  }
 
  getId(): string {
    return this.id;
  }
}
 
class User extends BaseModel implements Auditable {
  createdAt: Date;
  updatedAt: Date;
  private changeLog: string[] = [];
 
  constructor(
    id: string,
    public name: string,
    public email: string
  ) {
    super(id);
    this.createdAt = new Date();
    this.updatedAt = new Date();
  }
 
  logChange(action: string): void {
    this.changeLog.push(`${new Date().toISOString()}: ${action}`);
    this.updatedAt = new Date();
  }
 
  updateEmail(newEmail: string): void {
    this.email = newEmail;
    this.logChange(`Email updated to ${newEmail}`);
  }
 
  getChanges(): string[] {
    return [...this.changeLog];
  }
}
 
const user = new User("u1", "Alice", "[email protected]");
user.updateEmail("[email protected]");
console.log(user.getChanges());

User inherits id and getId() from BaseModel, and must implement all Auditable members.

Multiple Interfaces with Inheritance

interface Serializable {
  toJSON(): string;
}
 
interface Comparable {
  compareTo(other: Comparable): number;
}
 
class Entity {
  constructor(protected id: string) {}
 
  getId(): string {
    return this.id;
  }
}
 
class Product extends Entity implements Serializable, Comparable {
  constructor(
    id: string,
    public name: string,
    public price: number
  ) {
    super(id);
  }
 
  toJSON(): string {
    return JSON.stringify({
      id: this.id,
      name: this.name,
      price: this.price,
    });
  }
 
  compareTo(other: Product): number {
    return this.price - other.price;
  }
}
 
const p1 = new Product("p1", "Laptop", 1200);
const p2 = new Product("p2", "Mouse", 25);
 
console.log(p1.toJSON()); // '{"id":"p1","name":"Laptop","price":1200}'
console.log(p1.compareTo(p2)); // 1175

Best Practices and Guidelines

Design Guidelines

✅ Keep interfaces focused

// ✅ Good: Single responsibility
interface Readable {
  read(): string;
}
 
interface Writable {
  write(data: string): void;
}
 
// ❌ Bad: Too many responsibilities
interface FileSystem {
  read(): string;
  write(data: string): void;
  delete(): void;
  move(path: string): void;
  copy(path: string): void;
  // Too much!
}

✅ Use interface segregation

// ✅ Good: Clients only depend on what they need
interface CanFly {
  fly(): void;
}
 
interface CanSwim {
  swim(): void;
}
 
interface CanWalk {
  walk(): void;
}
 
class Duck implements CanFly, CanSwim, CanWalk {
  fly(): void {
    /* ... */
  }
  swim(): void {
    /* ... */
  }
  walk(): void {
    /* ... */
  }
}
 
class Fish implements CanSwim {
  swim(): void {
    /* ... */
  }
}

✅ Prefer interfaces for public contracts

// ✅ Good: Interface defines contract
interface Logger {
  log(message: string): void;
}
 
class Service {
  constructor(private logger: Logger) {}
  // Can accept any logger implementation
}
 
// ❌ Bad: Depends on concrete class
class ServiceBad {
  constructor(private logger: ConsoleLogger) {}
  // Tightly coupled
}

✅ Use meaningful interface names

// ✅ Good: Clear purpose
interface Validator {}
interface Formatter {}
interface Serializable {}
 
// ❌ Bad: Generic names
interface IInterface {}
interface Helper {}
interface Utility {}

Interface Segregation Principle

Don't force classes to implement methods they don't need. Split large interfaces into smaller, focused ones. This makes your code more flexible and easier to test.

What's Next

You now understand how to implement interfaces in classes. In the next posts, you'll learn:

  • Parameter properties shorthand
  • Mixins for multiple inheritance
  • Decorators for metadata
  • Advanced type manipulation

Key Takeaways

Implementing interfaces gives you:

✅ Contracts - enforce that classes provide required members ✅ Multiple inheritance - implement many interfaces in one class ✅ Structural typing - duck typing for flexibility ✅ Decoupling - depend on abstractions, not concrete classes ✅ Polymorphism - treat different classes through common interfaces ✅ Type safety - compile-time checking of contracts

Use interfaces to define contracts without implementation. Implement multiple interfaces to gain multiple capabilities. Combine interfaces with inheritance when you need both contracts and shared code. Keep interfaces small and focused—one responsibility per interface. And remember: TypeScript's structural typing means objects satisfy interfaces by having the right shape, not by explicit declaration.