OOP Interview Prep
Abstraction

Interface

The Contract That Shapes Your Code

LinkedIn Hook

Most developers can implement an interface. Far fewer can explain why it exists.

Here's a question that separates junior from senior in OOP interviews:

"If an abstract class can also enforce method signatures, why do we need interfaces at all?"

The answer reveals whether you understand contracts, or just syntax.

Interfaces define what a type must do — not how it does it. No state. No logic. Just a guarantee. And that guarantee is the foundation of every plugin system, every adapter, every payment gateway abstraction you've ever used in production.

In this lesson you'll learn how interfaces work, how to implement multiple of them, and how JavaScript fakes the same behavior through duck typing.

Read the full lesson → [link]

#OOP #TypeScript #JavaScript #SoftwareEngineering #InterviewPrep


Interface thumbnail


What You'll Learn

  • What an interface is and the problem it solves in OOP design
  • How to define and implement a TypeScript interface with full syntax coverage
  • How to implement multiple interfaces on a single class
  • How JavaScript uses duck typing to achieve the same structural guarantees without syntax
  • The key difference between an interface and an abstract class (full breakdown comes in Lesson 3.4)

The Analogy That Makes Interfaces Click

Imagine a job contract. Before you hire anyone, you write down exactly what skills the role requires: "must be able to write(), review(), and publish()." You don't care whether the person went to university or learned online. You don't care how they learned those skills. You just need the guarantee that those capabilities exist.

That is exactly what an interface is. It defines the contract. Any class that signs the contract must deliver every method listed. The interface itself carries zero implementation. It's the spec sheet, not the product.

[INTERNAL-LINK: abstraction overview → Lesson 3.1: Abstraction]


What Is an Interface?

An interface is a pure structural contract. It lists method signatures and property types that a class must implement. It contains no method bodies, no state, no constructor logic. Nothing runs inside an interface. It only describes what shape a type must have.

In TypeScript, the interface keyword makes this explicit. In Java, interfaces work identically. JavaScript has no native interface syntax, but the same contract behavior emerges through duck typing, which we'll cover later in this lesson.

Interfaces are one of the cleanest tools in OOP for writing code that is open to extension. You code against the interface, not the concrete class. Swap the implementation, and the rest of the system never notices.

[INTERNAL-LINK: open/closed principle → Lesson 7.2: Open/Closed Principle]

Interface visual 1


Example 1 — Defining and Implementing a TypeScript Interface

// Define the interface — a pure contract with no implementation
interface Printable {
  print(): void;
  getPageCount(): number;
}

// Class 1 implements the contract — must provide all listed methods
class PDFDocument implements Printable {
  private pages: string[];

  constructor(pages: string[]) {
    this.pages = pages;
  }

  // Required by the interface — must be implemented
  print(): void {
    console.log(`Printing PDF with ${this.pages.length} pages...`);
  }

  // Required by the interface — must be implemented
  getPageCount(): number {
    return this.pages.length;
  }
}

// Class 2 also implements the same contract — different internal logic
class HTMLDocument implements Printable {
  private content: string;
  private estimatedPages: number;

  constructor(content: string, estimatedPages: number) {
    this.content = content;
    this.estimatedPages = estimatedPages;
  }

  print(): void {
    console.log(`Rendering HTML document to printer...`);
    console.log(`Content preview: ${this.content.substring(0, 50)}`);
  }

  getPageCount(): number {
    return this.estimatedPages;
  }
}

// The calling code only knows about the interface — not the concrete type
function sendToPrinter(doc: Printable): void {
  console.log(`Pages to print: ${doc.getPageCount()}`);
  doc.print();
}

const pdf = new PDFDocument(["Page 1", "Page 2", "Page 3"]);
const html = new HTMLDocument("<html><body>Hello World</body></html>", 2);

sendToPrinter(pdf);
// Output:
// Pages to print: 3
// Printing PDF with 3 pages...

sendToPrinter(html);
// Output:
// Pages to print: 2
// Rendering HTML document to printer...
// Content preview: <html><body>Hello World</body></html>

[PERSONAL EXPERIENCE] In practice, the most useful thing about this pattern is the sendToPrinter function. It accepts any Printable — a PDF, HTML page, image viewer, or a mock object in a test. You can pass a new document type six months later without touching a single line in sendToPrinter. That flexibility is why interfaces matter.


Example 2 — Implementing Multiple Interfaces

A class can only extend one parent in TypeScript and Java. But it can implement as many interfaces as needed. This is the primary reason interfaces exist as a separate concept from abstract classes.

// Interface 1 — defines serialization behavior
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

// Interface 2 — defines validation behavior
interface Validatable {
  validate(): boolean;
  getErrors(): string[];
}

// Interface 3 — defines logging behavior
interface Loggable {
  log(): void;
}

// One class fulfills all three contracts simultaneously
class UserProfile implements Serializable, Validatable, Loggable {
  private name: string;
  private email: string;
  private errors: string[] = [];

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

  // --- Serializable contract ---
  serialize(): string {
    return JSON.stringify({ name: this.name, email: this.email });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.name = parsed.name;
    this.email = parsed.email;
  }

  // --- Validatable contract ---
  validate(): boolean {
    this.errors = [];

    if (!this.name || this.name.trim().length === 0) {
      this.errors.push("Name cannot be empty");
    }
    if (!this.email.includes("@")) {
      this.errors.push("Email must contain @");
    }

    return this.errors.length === 0;
  }

  getErrors(): string[] {
    return this.errors;
  }

  // --- Loggable contract ---
  log(): void {
    console.log(`[UserProfile] name=${this.name} email=${this.email}`);
  }
}

const user = new UserProfile("Alice", "alice@example.com");

user.log();
// Output: [UserProfile] name=Alice email=alice@example.com

console.log(user.validate());
// Output: true

const serialized = user.serialize();
console.log(serialized);
// Output: {"name":"Alice","email":"alice@example.com"}

// Create a new user from serialized data
const restored = new UserProfile("", "");
restored.deserialize(serialized);
restored.log();
// Output: [UserProfile] name=Alice email=alice@example.com

// Test validation failure path
const badUser = new UserProfile("", "not-an-email");
console.log(badUser.validate());
// Output: false
console.log(badUser.getErrors());
// Output: [ 'Name cannot be empty', 'Email must contain @' ]

[UNIQUE INSIGHT] Implementing multiple interfaces is not just a workaround for the lack of multiple inheritance. It's a deliberate design choice. Each interface represents a distinct role. UserProfile plays three roles: something that can be saved, something that can be validated, and something that can be logged. Separating roles into distinct interfaces is also the foundation of the Interface Segregation Principle, covered in Lesson 7.4.


Example 3 — Interfaces with Optional Properties and Readonly Fields

TypeScript interfaces support property modifiers that add precision to the contract.

// Interface with optional and readonly properties
interface ButtonConfig {
  readonly id: string;      // Cannot be changed after assignment
  label: string;            // Required
  onClick: () => void;      // Required
  color?: string;           // Optional — may or may not be present
  disabled?: boolean;       // Optional — defaults handled by implementer
}

// A function that accepts any object matching the ButtonConfig shape
function renderButton(config: ButtonConfig): void {
  const colorStr = config.color ?? "default";
  const disabledStr = config.disabled ? " [DISABLED]" : "";
  console.log(
    `Button [${config.id}]: "${config.label}"` +
    ` | color=${colorStr}${disabledStr}`
  );
}

// Object literal — TypeScript checks it against ButtonConfig at compile time
const submitBtn: ButtonConfig = {
  id: "btn-submit",
  label: "Submit",
  onClick: () => console.log("Submitted!"),
  color: "#00ff87",
};

const cancelBtn: ButtonConfig = {
  id: "btn-cancel",
  label: "Cancel",
  onClick: () => console.log("Cancelled!"),
  disabled: true,
  // color is omitted — it's optional
};

renderButton(submitBtn);
// Output: Button [btn-submit]: "Submit" | color=#00ff87

renderButton(cancelBtn);
// Output: Button [btn-cancel]: "Cancel" | color=default [DISABLED]

// TypeScript prevents this at compile time:
// submitBtn.id = "new-id"; // Error: Cannot assign to 'id' because it is a read-only property

Interface visual 2


Example 4 — Duck Typing in JavaScript (No Interface Keyword Needed)

JavaScript has no interface keyword. But it achieves the same structural contract through duck typing. The concept comes from the saying: "if it walks like a duck and quacks like a duck, it's a duck." JavaScript only checks whether the methods you call actually exist on the object at runtime.

// No interface keyword — just a comment describing the expected shape
// Expected shape: { area(): number, describe(): string }

// Shape 1 — a circle object
const circle = {
  radius: 5,
  area() {
    return Math.PI * this.radius * this.radius;
  },
  describe() {
    return `Circle with radius ${this.radius}`;
  },
};

// Shape 2 — a rectangle object
const rectangle = {
  width: 4,
  height: 6,
  area() {
    return this.width * this.height;
  },
  describe() {
    return `Rectangle ${this.width}x${this.height}`;
  },
};

// Shape 3 — a triangle, implemented as a class
class Triangle {
  constructor(base, height) {
    this.base = base;
    this.height = height;
  }

  area() {
    return 0.5 * this.base * this.height;
  }

  describe() {
    return `Triangle with base ${this.base} and height ${this.height}`;
  }
}

// This function doesn't care what type it receives — only whether the methods exist
function printShapeInfo(shape) {
  // Duck typing: we assume the object has area() and describe()
  // If it doesn't, we get a runtime error — not a compile-time error
  console.log(shape.describe());
  console.log(`Area: ${shape.area().toFixed(2)}`);
  console.log("---");
}

const triangle = new Triangle(8, 3);

const shapes = [circle, rectangle, triangle];
shapes.forEach(printShapeInfo);

// Output:
// Circle with radius 5
// Area: 78.54
// ---
// Rectangle 4x6
// Area: 24.00
// ---
// Triangle with base 8 and height 3
// Area: 12.00
// ---

[ORIGINAL DATA] The duck typing pattern works because JavaScript's function dispatch is purely name-based at runtime. No compiler verifies the contract. The risk is that a missing method only surfaces as a runtime TypeError, not a build-time failure. TypeScript interfaces exist precisely to catch this class of bug before the code runs.


The Citation Capsule

Interfaces as Structural Contracts: An interface defines a named set of method signatures and property types that a class must implement, with zero implementation logic of its own. TypeScript's structural type system enforces interface compliance at compile time, while JavaScript relies on duck typing to achieve similar polymorphic behavior at runtime. A single class may implement multiple interfaces simultaneously, making interfaces the primary mechanism for multiple-type compliance in languages that prohibit multiple inheritance. (TypeScript Handbook, Microsoft, 2024)


Common Mistakes

  • Putting implementation logic inside an interface. An interface is a contract — method signatures only. No method bodies, no default values for methods, no constructor logic. If you need shared implementation, use an abstract class instead.

  • Treating TypeScript interfaces as runtime constructs. TypeScript interfaces are erased during compilation. They exist only at the type-checking stage. You cannot use instanceof against an interface at runtime. This trips up developers coming from Java.

    interface Printable { print(): void; }
    class PDFDocument implements Printable { print() {} }
    
    const doc = new PDFDocument();
    
    // This works — PDFDocument is a runtime class
    console.log(doc instanceof PDFDocument); // true
    
    // This does NOT work — interfaces don't exist at runtime
    // console.log(doc instanceof Printable); // Error: 'Printable' only refers to a type
  • Confusing interfaces with abstract classes. An interface is a pure contract. An abstract class can mix both contracts (abstract methods) and shared implementation (concrete methods). Use an interface when you need a behavioral contract with no shared state. The full comparison is in Lesson 3.4.

  • Implementing an interface incompletely. If a class claims to implement an interface but omits any method, TypeScript throws a compile-time error. This is the whole point. Do not suppress these errors with type casts.

  • Naming interfaces with a capital-I prefix. In older Java-influenced codebases you'll see ISerializable. Modern TypeScript style guides (including Microsoft's own) recommend dropping the I prefix. Just use Serializable. The context makes the role clear.


Interview Questions

Q: What is an interface in OOP? An interface is a pure structural contract. It defines the method signatures and property types that a class must implement. It contains no implementation logic, no state, and no constructor. Any class that claims to implement the interface must provide a concrete body for every method listed. The interface guarantees the shape — not the behavior.

Q: Why can a class implement multiple interfaces but only extend one class? A class can only have one parent because inheriting from multiple classes creates the diamond problem — ambiguity about which parent's version of a method to use when both define it. Interfaces carry no implementation, so there's nothing to conflict. Two interfaces listing the same method signature simply require the implementing class to provide one concrete version. No ambiguity arises.

Q: How does JavaScript simulate interfaces without the interface keyword? JavaScript uses duck typing. A function that expects an object with a serialize() method doesn't check the type — it just calls serialize(). If the method exists, the code works. If it doesn't, a TypeError is thrown at runtime. TypeScript adds compile-time enforcement of these structural contracts through its interface keyword and structural type system.

Q: What happens to TypeScript interfaces at runtime? They disappear. TypeScript is a compile-time type system. Interfaces are erased during the TypeScript-to-JavaScript compilation step. At runtime, there is no interface object, no metadata, and no way to use instanceof against an interface. They serve only the developer and the compiler during the build phase.

Q: When would you choose an interface over an abstract class? Choose an interface when you need to define a behavioral contract with zero shared state or implementation. Choose an abstract class when you need to share code among related classes while still enforcing certain method signatures. The rule of thumb: if it's purely "what a type must do," use an interface. If it's "what related types share plus what they must do," use an abstract class. Full decision framework in Lesson 3.4.


Quick Reference — Cheat Sheet

INTERFACE FUNDAMENTALS
-----------------------------------------------------------
Definition        Pure contract — method signatures + property types only
Implementation    Zero — no method bodies, no constructors, no state
Instantiation     Cannot instantiate an interface directly
Keyword           `interface` in TypeScript (none in JavaScript)
Runtime           Erased at compile time — does not exist in JS output
-----------------------------------------------------------

TYPESCRIPT INTERFACE SYNTAX
-----------------------------------------------------------
// Basic interface
interface Flyable {
  fly(): void;
  getAltitude(): number;
}

// Implementing one interface
class Plane implements Flyable {
  fly(): void { ... }
  getAltitude(): number { ... }
}

// Implementing multiple interfaces
class Drone implements Flyable, Serializable {
  fly(): void { ... }
  getAltitude(): number { ... }
  serialize(): string { ... }
  deserialize(data: string): void { ... }
}

// Optional and readonly properties
interface Config {
  readonly id: string;   // Cannot be reassigned after creation
  name: string;          // Required
  timeout?: number;      // Optional
}

// Interface extending another interface
interface Printable extends Loggable {
  print(): void;         // Adds to Loggable's contract
}
-----------------------------------------------------------

DUCK TYPING IN JAVASCRIPT
-----------------------------------------------------------
// No syntax — just behavioral matching at runtime
function process(entity) {
  entity.validate();   // Works if method exists, TypeError if not
  entity.save();
}
// Any object with validate() and save() satisfies the contract
-----------------------------------------------------------

INTERFACE vs ABSTRACT CLASS (QUICK SUMMARY)
-----------------------------------------------------------
Interface         No implementation, multiple allowed, no state
Abstract Class    Can have implementation, one parent, can have state
Rule of thumb     Interface = "must do", Abstract = "related + must do"
-----------------------------------------------------------

COMMON MODIFIERS
-----------------------------------------------------------
readonly    Property cannot be changed after initial assignment
?           Optional — does not have to be present
()=>        Method signature without return type body
-----------------------------------------------------------

Previous: Lesson 3.2 → Next: Lesson 3.4 →


This is Lesson 3.3 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page