Chapter 4.4☕ 15 min read

Implementing Interfaces

The blueprint demands, the class delivers — no missing minars allowed!

01The Royal Decree & Builder

In TypeScript, interfaces define WHAT a shape looks like — the properties it must have, the methods it must expose. Classes define HOW it works — the actual logic, the implementation details. The implements keyword is the bridge that brings them together. When a class implements an interface, it makes a promise: "I will provide everything this interface demands."

Think of it like the Nizam's chief architect issuing a royal decree: "Every palace in my kingdom MUST have 4 minars, a grand gate, and a durbar hall." That decree is the interface — the contract, the requirements. The builders of Charminar then go and ACTUALLY BUILD it — they implements the PalaceBlueprint. If they forget the 4th minar, the Nizam's inspector (the TypeScript compiler) rejects the construction:

// The Nizam's royal decree
interface PalaceBlueprint {
  minars: number;
  grandGate: boolean;
  durbarHall: string;
}

// The builders implement the decree
class Charminar
  implements PalaceBlueprint {
  minars = 4;
  grandGate = true;
  durbarHall = "Royal Durbar";
  // Extra! Not in the blueprint
  mosque = true;
}

If they forget a required property, the inspector shouts: "Error: Property is missing!" The builders MUST include everything the blueprint demands — no shortcuts, no missing features. But they can also add EXTRA things the blueprint doesn't mention — Charminar has a mosque on the top floor, which wasn't in the basic blueprint. That's perfectly fine! Implementing means: satisfy ALL requirements, then add more if you want. This separation of "what" from "how" is powerful — it lets you design contracts first and build implementations later, just like the Nizam designed decrees before the first brick was laid. Every great palace starts with a blueprint, and every robust TypeScript class starts with an interface.

02Implementing Single Interface

The syntax for implementing a single interface is straightforward: class ClassName implements InterfaceName { ... }. The class MUST provide all properties and methods the interface declares — no exceptions, no excuses. Let's see this in action with a simple example:

interface Printable {
  print(): void;
}

class Report implements Printable {
  print() {
    console.log("Printing report");
  }
}

const doc = new Report();
doc.print(); // "Printing report"

Now let's add properties to the mix. When an interface declares a property, the implementing class must have that property with the exact same type:

interface Named {
  name: string;
}

class User implements Named {
  constructor(
    public name: string
  ) {}
}

const u = new User("Osman");
console.log(u.name); // "Osman"

If you forget to implement a required member, TypeScript immediately flags it:

interface Named {
  name: string;
}

// ❌ ERROR! Property 'name' missing
class Bad implements Named {}

The error message is clear: "Class 'Bad' incorrectly implements interface 'Named'. Property 'name' is missing." This is the Nizam's inspector catching the missing minar — you simply cannot skip required properties. The compiler enforces the contract strictly.

Types must match exactly. You can't substitute a different type — the blueprint says stone, you can't use glass:

interface Item {
  count: number;
}

// ❌ ERROR! count must be number
class Wrong implements Item {
  count = "five"; // string ≠ number!
}

But you can add extra members that the interface doesn't require. The interface is a minimum contract, not a maximum:

interface Named {
  name: string;
}

class User implements Named {
  constructor(
    public name: string,
    public age: number // extra!
  ) {}
}

// age is extra — totally fine!
const u = new User("Osman", 30);

Think of it this way: the interface says "you MUST have a name." The class says "I have a name AND an age." That's perfectly acceptable. The class goes beyond the minimum requirement, like adding a beautiful garden to a palace that only required four walls and a roof. The inspector only checks what's in the decree — anything extra is your own architectural flair!

03Implementing Multiple

A class can implement MULTIPLE interfaces — it must satisfy ALL of them. This is where TypeScript's contract system truly shines. The syntax uses commas: class X implements A, B, C { ... }. Every method and property from every interface must be present in the class, or TypeScript will flag the errors immediately.

interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

class SmartDevice
  implements Serializable, Loggable {
  serialize() {
    return JSON.stringify(this);
  }
  log() {
    console.log("Device logged");
  }
}

const phone = new SmartDevice();
phone.serialize(); // "{}"
phone.log(); // "Device logged"

If you miss even ONE method from any interface, TypeScript catches it. Every method from every interface is mandatory — no partial implementations allowed. This is like the Nizam demanding that every royal building must satisfy BOTH the fire safety decree AND the architectural beauty decree — you can't satisfy one and ignore the other. Both must be honored, or the inspector rejects your building.

A powerful real-world pattern is implementing a generic interface. This is heavily used in repository patterns, data layers, and service classes across production codebases:

interface Repository<T> {
  findById(id: string): T;
  save(item: T): void;
  delete(id: string): void;
}

interface User {
  id: string;
  name: string;
}

class UserRepository
  implements Repository<User> {
  private items: User[] = [];

  findById(id: string): User {
    return this.items.find(
      u => u.id === id
    )!;
  }

  save(item: User): void {
    this.items.push(item);
  }

  delete(id: string): void {
    this.items = this.items.filter(
      u => u.id !== id
    );
  }
}

Now UserRepository must provide all three methods with the exact signatures. If you implement Repository<Product> instead, the types change accordingly. The interface stays the same, but the implementation adapts — like using the same blueprint for different palaces across the kingdom, each with its own unique purpose but following the same royal standards.

There's also an advanced pattern where interfaces describe constructor signatures using the new keyword:

interface Constructable {
  new (name: string): any;
}

// Used in factory patterns
function createInstance(
  Ctor: Constructable,
  name: string
) {
  return new Ctor(name);
}

This is mainly used in factories and dependency injection — advanced patterns where you need to pass classes around as values. The new signature ensures the class can be instantiated with specific parameters. You won't use this daily, but knowing it helps when you encounter it in frameworks like Angular or NestJS, where dependency injection relies heavily on such patterns to wire things together automatically.

04Implementation Traps

Even experienced developers stumble on these traps when implementing interfaces. Let's go through each one carefully so you don't get caught at runtime or, worse, in an interview!

Trap 1: Implementing is a TYPE CHECK, not a runtime change. When a class implements an interface, TypeScript only checks the shape at compile time. At runtime, the class is still just a class — interfaces are completely erased. The implements keyword doesn't add any methods, doesn't modify this, doesn't change behavior. It's purely a compile-time safety net that catches mistakes before your code ever runs.

Trap 2: Access modifier mismatch. This one catches many developers off guard:

interface Secret {
  key: string;
}

// ❌ ERROR! 'key' is private,
// but interface says public
class Vault implements Secret {
  private key: string = "123";
}

Interface members are implicitly public. When you implement them, you can't make them private or protected — that would violate the contract. Imagine the Nizam's decree saying "the grand gate must be visible to all visitors," but the builder hides it behind a wall! The inspector won't allow it. If the interface declares a public member, the class must keep it public.

Trap 3: Method signature mismatch. The parameter types and return types must match exactly:

interface A {
  calc(x: number): string;
}

// ❌ ERROR! Parameter and return
// types don't match the interface
class B implements A {
  calc(x: string): number {
    return Number(x);
  }
}

The interface promised calc takes a number and returns a string. The class swaps both — that's a contract violation. Parameter types, return types, and even the number of parameters must align with what the interface specifies.

Trap 4: Optional interface members. An optional property (name?: string) gives flexibility — you don't HAVE to provide it, but if you do, the type must match:

interface Config {
  name?: string;
}

// ✅ Fine — providing a default
class Settings implements Config {
  name: string = "default";
}

// ✅ Also fine — omitting optional
class Minimal implements Config {}

Both are valid! The class can provide a default value even though the interface says it's optional, or it can omit it entirely. The optional marker just means the interface doesn't demand it.

Trap 5: Can't implement a union type. This is a hard rule — you can only implement object-shaped types:

type Thing = A | B;

// ❌ ERROR! Can only implement
// object types, not unions
class X implements Thing {}

A union type means "this OR that" — a class can't be "this OR that" simultaneously. It must commit to one specific shape, just like a palace can't be "a fort OR a mosque" — it must choose what it is and build accordingly. If you need flexible typing, use generic constraints or method overloads instead.

05Implements Cheatsheet

Let's consolidate everything about implementing interfaces into a quick-reference cheatsheet. Bookmark this section — you'll come back to it often:

// ── SINGLE INTERFACE ──
interface Named {
  name: string;
}

class User implements Named {
  constructor(
    public name: string
  ) {}
}

// ── MULTIPLE INTERFACES ──
interface Printable {
  print(): void;
}
interface Loggable {
  log(): void;
}

class Report
  implements Printable, Loggable {
  print() {
    console.log("doc");
  }
  log() {
    console.log("logged");
  }
}

// ── GENERIC INTERFACE ──
interface Repo<T> {
  findById(id: string): T;
  save(item: T): void;
}

class UserRepo
  implements Repo<User> {
  findById(id: string) { /* ... */ }
  save(item: User) { /* ... */ }
}

The Five Golden Rules of implements:

  • Rule 1: Must implement ALL required members — no skipping, no shortcuts. If the interface says you need a durbarHall, you build that durbar hall!
  • Rule 2: Can add EXTRA members not in the interface — the interface defines the minimum, not the maximum. Add your mosque on top if you want!
  • Rule 3: Access modifiers must be compatible — interface members are implicitly public, so implementations must be public too. No hiding the grand gate!
  • Rule 4: Method signatures must match exactly — same parameter types, same return type. The decree's specifications must be followed precisely.
  • Rule 5: Optional interface members can be provided, defaulted, or omitted — but if provided, the type must match.

The Golden Rule: Implementing is the builder's promise to the Nizam — every minar in the blueprint must be built. No shortcuts, bhai! But you can always add a mosque on top. The interface is the minimum contract, the class is the full reality. TypeScript enforces this promise at compile time, so by the time your code runs, every palace in the kingdom meets the royal standards. Use implements to make your contracts explicit and your code self-documenting — future developers will thank you for the clarity.

Key Takeaways

  • `implements` makes a class promise to fulfill an interface contract
  • All required interface members MUST appear in the class — no exceptions
  • Classes CAN add extra members beyond what the interface requires
  • Access modifiers must match — interface members are implicitly public
  • Method signatures (params + return type) must match exactly
  • Optional interface members can be provided, defaulted, or omitted
  • You cannot implement union types — only object-shaped types
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase