Skip to content

Interfaces Deep Dive

Interfaces are TypeScript’s dedicated tool for describing the shape of objects. They support extension, merging, and structural typing, making them ideal for modeling complex systems, public APIs, and OOP patterns.

Let’s explore these capabilities in depth.


Extending Interfaces

Interfaces can extend one another, allowing you to build reusable, composable object models.

Basic extension

interface Person {
  name: string
}

interface Employee extends Person {
  role: string
}

Now Employee includes both name and role.

Multiple inheritance

Interfaces can extend multiple interfaces:

interface Timestamped {
  createdAt: Date
}

interface Identified {
  id: string
}

interface User extends Timestamped, Identified {
  name: string
}

This is extremely useful for modeling:

  • database entities
  • domain models
  • shared traits across objects

Extending type aliases

Interfaces can even extend type aliases (as long as the alias resolves to an object type):

type Coordinates = { x: number; y: number }

interface Point extends Coordinates {
  label: string
}

This flexibility makes interfaces a great choice for building layered, evolving models.


Merging Interfaces

One of the most unique features of interfaces is declaration merging.

If you declare the same interface name twice, TypeScript merges them into a single interface.

Example: Merging

interface Settings {
  theme: string
}

interface Settings {
  language: string
}

The resulting type is:

interface Settings {
  theme: string
  language: string
}

Why merging matters

  • Library authors can extend existing interfaces
  • You can augment third‑party types
  • You can split large interfaces across files

Real‑world example: Extending Express Request

declare module "express-serve-static-core" {
  interface Request {
    userId?: string
  }
}

This adds userId to Express’s Request type—without modifying the library.

Important note

Type aliases cannot merge.

If you need merging or augmentation, use an interface.


Structural Typing vs Nominal Typing

This is one of the most important conceptual differences between TypeScript and languages like Java, C#, or Rust.

TypeScript uses structural typing

A type is compatible with another type if it has the same shape, regardless of its name.

Nominal typing (not used by TypeScript)

A type is compatible only if it has the same declared identity (e.g., same class or type name).


Structural Typing in Action

Example: Two types with the same shape

interface Point {
  x: number
  y: number
}

interface Vector {
  x: number
  y: number
}

const p: Point = { x: 1, y: 2 }
const v: Vector = p // ✔ Allowed

Even though Point and Vector are different names, they are structurally identical.

Extra properties are allowed when assigning to a wider type

interface Person {
  name: string
}

const obj = { name: "Alice", age: 30 }

const p: Person = obj // ✔ Allowed

This is called structural compatibility.


Why Structural Typing Matters

1. It makes TypeScript feel natural for JavaScript developers

JavaScript is dynamic and shape‑based; TypeScript embraces that.

2. It enables flexible, composable APIs

You don’t need rigid class hierarchies.

3. It reduces boilerplate

You don’t need explicit “implements” everywhere.

4. It makes testing easier

Mocks and stubs only need the required properties.


When Structural Typing Can Surprise You

Example: Accidental compatibility

interface Credentials {
  username: string
  password: string
}

interface ApiKey {
  username: string
  key: string
}

const key: ApiKey = { username: "a", key: "123" }
const creds: Credentials = key // ❌ Error? No — ✔ Allowed!

This is allowed because both types have a username: string.

If you need stricter checks, you can:

  • add a discriminant field
  • use branded types
  • use classes (which behave more nominally)

Putting It All Together

Here’s a realistic example combining extension, merging, and structural typing:

interface Entity {
  id: string
}

interface Timestamped {
  createdAt: Date
  updatedAt: Date
}

interface User extends Entity, Timestamped {
  name: string
  email: string
}

// Declaration merging
interface User {
  isAdmin?: boolean
}

function printUser(u: User) {
  console.log(u.id, u.name, u.createdAt)
}

const obj = {
  id: "1",
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date(),
  updatedAt: new Date(),
  isAdmin: true
}

printUser(obj) // ✔ Works due to structural typing

This example demonstrates:

  • interface extension
  • interface merging
  • structural compatibility
  • real‑world modeling

Summary

In this lesson, you learned the deeper capabilities of TypeScript interfaces:

1. Extending interfaces

  • Build reusable, layered object models
  • Support multiple inheritance
  • Can extend type aliases

2. Merging interfaces

  • Unique to interfaces
  • Enables augmentation of third‑party types
  • Useful for large or modular codebases

3. Structural typing

  • Types are compatible based on shape, not name
  • Makes TypeScript flexible and JavaScript‑friendly
  • Can be controlled with discriminants or branded types

Interfaces are one of the most powerful modeling tools in TypeScript, and mastering them unlocks cleaner, safer, and more expressive code.