TypeScriptImplementing Interfaces

Implementing Interfaces

The implements keyword is how a TypeScript class declares a contract: "I guarantee that I provide everything this interface requires." The compiler then verifies that guarantee at every class definition and every usage site.

Implementing interfaces is the foundation of programming to abstractions — code that depends on interfaces rather than concrete classes is more testable, modular, and extensible.

Basic Interface Implementation

An interface describes a shape — property names, types, and method signatures. A class that implements it must satisfy every member of that interface.

TS
interface Printable {
  print(): void;
}

interface Serializable {
  serialize(): string;
  deserialize(data: string): this;
}

class Document implements Printable, Serializable {
  constructor(private content: string) {}

  print(): void {
    console.log(this.content);
  }

  serialize(): string {
    return JSON.stringify({ content: this.content });
  }

  deserialize(data: string): this {
    const parsed = JSON.parse(data) as { content: string };
    return new (this.constructor as new (content: string) => this)(parsed.content);
  }
}

const doc = new Document('Hello, World!');
doc.print();                        // Hello, World!
const json = doc.serialize();       // '{"content":"Hello, World!"}'
const restored = doc.deserialize(json);
restored.print();                   // Hello, World!
Note
A class can implement multiple interfaces, separated by commas. It must satisfy all of them simultaneously.
What Implements Checks — and What It Doesn't

implements checks that the class has the right shape. It does NOT:

  • Copy interface properties or methods into the class
  • Affect runtime behaviour in any way
  • Guarantee that the class correctly satisfies the interface's semantic contract (only the structural contract)

TS
interface Resettable {
  reset(): void;
  readonly defaultValue: string;
}

class Counter implements Resettable {
  defaultValue = 'zero'; // must be present — checked by implements
  private count = 0;

  reset(): void {
    this.count = 0; // must have this method — checked
  }

  increment() {
    this.count++;
  }

  // Classes can have MORE members than the interface requires:
  decrement() { this.count--; }
  get value() { return this.count; }
}

// Using the class as the interface type narrows what's visible:
const r: Resettable = new Counter();
r.reset();        // ✅ visible through interface
r.defaultValue;   // ✅ visible through interface
// r.increment(); // ❌ Error — 'increment' is not on Resettable
Interface vs implements: A Common Misconception

A common mistake is thinking implements means the class inherits from the interface. It does not. implements is purely a compile-time contract check.

TS
interface Logger {
  log(message: string): void;
  warn(message: string): void;
  error(message: string): void;
}

// ❌ WRONG — implements does NOT add these methods to the class
class ConsoleLogger implements Logger {
  // If you forget to add these, you get a compile error
}
// Error: Class 'ConsoleLogger' incorrectly implements interface 'Logger'.
//   Property 'log' is missing in type 'ConsoleLogger' but required in type 'Logger'.

// ✅ CORRECT — you must explicitly provide all interface members
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
  warn(message: string): void {
    console.warn(`[WARN] ${message}`);
  }
  error(message: string): void {
    console.error(`[ERROR] ${message}`);
  }
}
Interfaces with Optional Members

Optional interface members (marked with ?) do not need to be implemented. If a class does implement them, they must match the correct type.

TS
interface DataSource<T> {
  fetch(id: string): Promise<T>;
  fetchAll(): Promise<T[]>;
  create?(item: Omit<T, 'id'>): Promise<T>;  // optional
  update?(id: string, patch: Partial<T>): Promise<T>;  // optional
  delete?(id: string): Promise<void>;  // optional
}

// Minimal implementation — only required members
class ReadOnlyUserSource implements DataSource<User> {
  async fetch(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }

  async fetchAll(): Promise<User[]> {
    const res = await fetch('/api/users');
    return res.json();
  }
  // No create/update/delete — that's fine, they're optional
}

// Full implementation
class UserRepository implements DataSource<User> {
  async fetch(id: string): Promise<User> { /* ... */ return {} as User; }
  async fetchAll(): Promise<User[]> { /* ... */ return []; }
  async create(data: Omit<User, 'id'>): Promise<User> { /* ... */ return {} as User; }
  async update(id: string, patch: Partial<User>): Promise<User> { /* ... */ return {} as User; }
  async delete(id: string): Promise<void> { /* ... */ }
}
Implementing Generic Interfaces

Classes can implement generic interfaces, either keeping the type parameter generic themselves or fixing it to a concrete type.

TS
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(filter?: Partial<T>): Promise<T[]>;
  save(item: T): Promise<T>;
  remove(id: string): Promise<void>;
}

// Option A: Class is also generic (flexible, reusable)
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
  private store = new Map<string, T>();

  async findById(id: string): Promise<T | null> {
    return this.store.get(id) ?? null;
  }

  async findAll(filter?: Partial<T>): Promise<T[]> {
    const items = [...this.store.values()];
    if (!filter) return items;
    return items.filter(item =>
      Object.entries(filter).every(([k, v]) => item[k as keyof T] === v)
    );
  }

  async save(item: T): Promise<T> {
    this.store.set(item.id, item);
    return item;
  }

  async remove(id: string): Promise<void> {
    this.store.delete(id);
  }
}

// Option B: Class fixes the type parameter (concrete, single-purpose)
interface User { id: string; name: string; email: string }

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> { /* ... */ return null; }
  async findAll(): Promise<User[]> { /* ... */ return []; }
  async save(user: User): Promise<User> { /* ... */ return user; }
  async remove(id: string): Promise<void> { /* ... */ }
}

// Usage
const userRepo = new InMemoryRepository<User>();
await userRepo.save({ id: '1', name: 'Alice', email: 'alice@example.com' });
const found = await userRepo.findById('1');
console.log(found?.name); // Alice
Interface Segregation Principle

The Interface Segregation Principle (the I in SOLID) says: clients should not be forced to depend on methods they don't use. Compose small, focused interfaces rather than one large one.

TS
// ❌ Fat interface — every implementor must provide everything
interface FileSystem {
  read(path: string): Buffer;
  write(path: string, data: Buffer): void;
  delete(path: string): void;
  createDirectory(path: string): void;
  list(path: string): string[];
  stat(path: string): FileStat;
  watch(path: string, cb: () => void): Watcher;
}

// ✅ Segregated interfaces — implement only what you need
interface Readable { read(path: string): Buffer }
interface Writable { write(path: string, data: Buffer): void }
interface Deletable { delete(path: string): void }
interface Listable { list(path: string): string[] }
interface Watchable { watch(path: string, cb: () => void): Watcher }

// Full filesystem implementation
class LocalFileSystem implements Readable, Writable, Deletable, Listable, Watchable {
  read(path: string): Buffer { return Buffer.alloc(0); }
  write(path: string, data: Buffer): void {}
  delete(path: string): void {}
  list(path: string): string[] { return []; }
  watch(path: string, cb: () => void): Watcher { return { stop() {} }; }
}

// In-memory test double — only what tests need
class MemoryFileSystem implements Readable, Writable {
  private files = new Map<string, Buffer>();
  read(path: string): Buffer { return this.files.get(path) ?? Buffer.alloc(0); }
  write(path: string, data: Buffer): void { this.files.set(path, data); }
}

// A backup service only needs Readable
function backup(source: Readable, destination: Writable) {
  const data = source.read('/data/backup.json');
  destination.write('/backups/backup.json', data);
}
Success
Segregated interfaces make testing dramatically easier — you can provide a minimal stub that only implements what the test exercises, rather than mocking an entire fat interface.
Interfaces with Readonly Members

TS
interface Coordinate {
  readonly lat: number;
  readonly lng: number;
  distanceTo(other: Coordinate): number;
}

class GeoPoint implements Coordinate {
  constructor(
    public readonly lat: number,
    public readonly lng: number,
  ) {}

  distanceTo(other: Coordinate): number {
    const R = 6371; // Earth radius in km
    const dLat = ((other.lat - this.lat) * Math.PI) / 180;
    const dLng = ((other.lng - this.lng) * Math.PI) / 180;
    const a =
      Math.sin(dLat / 2) ** 2 +
      Math.cos((this.lat * Math.PI) / 180) *
        Math.cos((other.lat * Math.PI) / 180) *
        Math.sin(dLng / 2) ** 2;
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  }
}

const london = new GeoPoint(51.507, -0.128);
const paris  = new GeoPoint(48.857, 2.352);
console.log(london.distanceTo(paris).toFixed(0)); // ~341 km
Using Interfaces as Function Parameters

The real power of implements is that it lets you write functions and services that accept any class satisfying an interface — not just one specific class. This is dependency inversion.

TS
interface EmailSender {
  send(to: string, subject: string, body: string): Promise<void>;
}

// Production implementation
class SendGridEmailSender implements EmailSender {
  constructor(private readonly apiKey: string) {}

  async send(to: string, subject: string, body: string): Promise<void> {
    await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ to, subject, content: [{ type: 'text/html', value: body }] }),
    });
  }
}

// Test double — no real emails sent
class MockEmailSender implements EmailSender {
  public sentEmails: Array<{ to: string; subject: string; body: string }> = [];

  async send(to: string, subject: string, body: string): Promise<void> {
    this.sentEmails.push({ to, subject, body });
  }
}

// Service depends on the interface, not the concrete class
class AuthService {
  constructor(private readonly emailSender: EmailSender) {}

  async sendVerificationEmail(userEmail: string, token: string): Promise<void> {
    const link = `https://app.example.com/verify?token=${token}`;
    await this.emailSender.send(
      userEmail,
      'Verify your email',
      `<a href="${link}">Click here to verify</a>`,
    );
  }
}

// Test — inject mock
const mock = new MockEmailSender();
const auth = new AuthService(mock);
await auth.sendVerificationEmail('user@example.com', 'abc123');
console.log(mock.sentEmails[0].subject); // 'Verify your email'

// Production — inject real sender
const real = new SendGridEmailSender(process.env.SENDGRID_API_KEY!);
const prodAuth = new AuthService(real);
Summary
  • implements declares a structural contract — the compiler verifies the class provides every interface member.

  • A class can implement multiple interfaces separated by commas.

  • implements does NOT copy interface members into the class — you must write every method yourself.

  • Classes can have more members than the interface requires; only the required ones are checked.

  • Optional interface members (marked with ?) do not need to be implemented.

  • Implement generic interfaces by keeping the type parameter generic on the class or fixing it to a concrete type.

  • Prefer many small, focused interfaces (Interface Segregation Principle) over one large interface.

  • Depending on interfaces rather than concrete classes enables dependency inversion and easy testing.