TypeScript by Example

Interfaces

Declaring interfaces - optional and readonly properties, extending interfaces, and declaration merging.

Interfaces describe the shape of an object. TypeScript uses structural typing - any object that has the required fields satisfies the interface, regardless of what class or constructor created it.

An interface declares a set of required properties and their types. Optional properties use ?; readonly properties use readonly and cannot be reassigned after creation.

interface User {
  id: string;
  name: string;
  email?: string;         // optional
  readonly createdAt: Date; // cannot be changed after assignment
}
 
const user: User = {
  id: "u_1",
  name: "Alice",
  createdAt: new Date(),
};
 
user.name = "Bob";          // ok - name is mutable
// user.createdAt = new Date(); // error - readonly

Interfaces can extend other interfaces to compose shapes without repeating fields. A class can implement an interface to guarantee it satisfies the contract.

interface Entity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}
 
interface Product extends Entity {
  name: string;
  priceInCents: number;
  sku: string;
}
 
// Any object with all required fields satisfies Product - no class needed
const widget: Product = {
  id: "p_1",
  name: "Widget",
  priceInCents: 999,
  sku: "WGT-001",
  createdAt: new Date(),
  updatedAt: new Date(),
};

Declaration merging: declaring the same interface twice merges the two declarations into one. This is how library authors augment global types without modifying the original source.

interface Config {
  port: number;
}
 
interface Config {
  host: string; // merged - both declarations combine
}
 
// Config now requires: { port: number; host: string }
const config: Config = { port: 3000, host: "localhost" };

In production

Interfaces are open by design - declaration merging is how libraries like Express augment Request to add req.user without forking the types. That power cuts both ways: if you accidentally re-declare an interface you own, TypeScript silently merges the declarations rather than erroring, which can add unexpected properties to an otherwise closed contract. For shapes you own and don't want extended by consumers or test utilities, prefer type aliases - they're closed and don't merge.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free