1. javascript
  2. /typescript
  3. /interfaces

TypeScript Interfaces

Interfaces in TypeScript provide a powerful way to define the structure of objects, creating contracts that ensure consistency across your application. They serve as blueprints that describe what properties and methods an object should have, enabling better code organization, documentation, and type safety.

Unlike classes, interfaces exist only at compile time and are used purely for type checking. They help define clear API boundaries, make code more readable, and enable powerful IDE features like autocompletion and refactoring support.

Basic Interface Definition

Interfaces define the shape of objects by specifying property names and their types.

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// Using the interface
function createUser(userData: User): User {
  return {
    id: userData.id,
    name: userData.name,
    email: userData.email,
    isActive: userData.isActive
  };
}

const newUser: User = {
  id: 1,
  name: "Alice Johnson",
  email: "[email protected]",
  isActive: true
};

Optional Properties

Properties can be marked as optional using the ? operator, making them not required when implementing the interface.

interface Product {
  id: number;
  name: string;
  description?: string; // Optional property
  price: number;
  category?: string; // Optional property
  inStock: boolean;
}

// Valid implementations
const product1: Product = {
  id: 1,
  name: "Laptop",
  price: 999.99,
  inStock: true
};

const product2: Product = {
  id: 2,
  name: "Mouse",
  description: "Wireless gaming mouse",
  price: 49.99,
  category: "Electronics",
  inStock: false
};

Readonly Properties

Properties can be marked as readonly to prevent modification after object creation.

interface Point {
  readonly x: number;
  readonly y: number;
}

interface Configuration {
  readonly apiUrl: string;
  readonly timeout: number;
  retryCount?: number; // Can be modified
}

const origin: Point = { x: 0, y: 0 };
// origin.x = 1; // Error: Cannot assign to 'x' because it is a read-only property

const config: Configuration = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retryCount: 3
};

// config.apiUrl = "new-url"; // Error: readonly property
config.retryCount = 5; // OK: not readonly

Function Types in Interfaces

Interfaces can define function signatures as properties or methods.

interface Calculator {
  // Method syntax
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
  
  // Property syntax
  multiply: (a: number, b: number) => number;
  divide: (a: number, b: number) => number;
}

class BasicCalculator implements Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
  
  subtract(a: number, b: number): number {
    return a - b;
  }
  
  multiply = (a: number, b: number): number => {
    return a * b;
  }
  
  divide = (a: number, b: number): number => {
    if (b === 0) throw new Error("Division by zero");
    return a / b;
  }
}

Index Signatures

Index signatures allow interfaces to describe objects with dynamic property names.

interface StringDictionary {
  [key: string]: string;
}

interface NumberDictionary {
  [key: string]: number;
}

interface FlexibleUser {
  name: string;
  email: string;
  [key: string]: any; // Additional properties of any type
}

const translations: StringDictionary = {
  hello: "Hola",
  goodbye: "Adiós",
  welcome: "Bienvenido"
};

const scores: NumberDictionary = {
  math: 95,
  science: 88,
  english: 92
};

const user: FlexibleUser = {
  name: "John",
  email: "[email protected]",
  age: 30, // Additional property
  city: "New York" // Additional property
};

Interface Inheritance

Interfaces can extend other interfaces, creating hierarchical type relationships.

interface Animal {
  name: string;
  age: number;
}

interface Mammal extends Animal {
  furColor: string;
  warmBlooded: boolean;
}

interface Dog extends Mammal {
  breed: string;
  bark(): void;
}

const myDog: Dog = {
  name: "Buddy",
  age: 3,
  furColor: "golden",
  warmBlooded: true,
  breed: "Golden Retriever",
  bark() {
    console.log("Woof!");
  }
};

// Multiple inheritance
interface Flyable {
  maxAltitude: number;
  fly(): void;
}

interface Swimmable {
  maxDepth: number;
  swim(): void;
}

interface Duck extends Animal, Flyable, Swimmable {
  quack(): void;
}

Generic Interfaces

Interfaces can be generic, allowing them to work with different types while maintaining type safety.

interface Container<T> {
  value: T;
  getValue(): T;
  setValue(value: T): void;
}

interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  timestamp: Date;
}

interface Repository<T, K> {
  findById(id: K): Promise<T | null>;
  save(entity: T): Promise<T>;
  delete(id: K): Promise<boolean>;
  findAll(): Promise<T[]>;
}

// Usage examples
const stringContainer: Container<string> = {
  value: "Hello",
  getValue() {
    return this.value;
  },
  setValue(value: string) {
    this.value = value;
  }
};

const userResponse: ApiResponse<User[]> = {
  success: true,
  data: [newUser],
  message: "Users retrieved successfully",
  timestamp: new Date()
};

class UserRepository implements Repository<User, number> {
  async findById(id: number): Promise<User | null> {
    // Implementation
    return null;
  }
  
  async save(user: User): Promise<User> {
    // Implementation
    return user;
  }
  
  async delete(id: number): Promise<boolean> {
    // Implementation
    return true;
  }
  
  async findAll(): Promise<User[]> {
    // Implementation
    return [];
  }
}

Interfaces vs Type Aliases

While interfaces and type aliases can often be used interchangeably, they have different capabilities and use cases.

// Interface approach
interface UserInterface {
  id: number;
  name: string;
  email: string;
}

// Type alias approach
type UserType = {
  id: number;
  name: string;
  email: string;
};

// Interfaces can be extended and merged
interface UserInterface {
  createdAt: Date; // Declaration merging
}

interface ExtendedUser extends UserInterface {
  isAdmin: boolean;
}

// Type aliases are more flexible with unions
type Status = "pending" | "approved" | "rejected";
type ID = string | number;

type UserWithStatus = UserType & {
  status: Status;
  id: ID;
};

Implementing Interfaces in Classes

Classes can implement interfaces to ensure they conform to specific contracts.

interface Drivable {
  speed: number;
  start(): void;
  stop(): void;
  accelerate(amount: number): void;
}

interface Flyable {
  altitude: number;
  takeOff(): void;
  land(): void;
}

class Car implements Drivable {
  speed: number = 0;
  
  start(): void {
    console.log("Car engine started");
  }
  
  stop(): void {
    this.speed = 0;
    console.log("Car stopped");
  }
  
  accelerate(amount: number): void {
    this.speed += amount;
    console.log(`Car accelerated to ${this.speed} mph`);
  }
}

class FlyingCar implements Drivable, Flyable {
  speed: number = 0;
  altitude: number = 0;
  
  // Drivable methods
  start(): void {
    console.log("Flying car started");
  }
  
  stop(): void {
    this.speed = 0;
    console.log("Flying car stopped");
  }
  
  accelerate(amount: number): void {
    this.speed += amount;
  }
  
  // Flyable methods
  takeOff(): void {
    this.altitude = 1000;
    console.log("Flying car took off");
  }
  
  land(): void {
    this.altitude = 0;
    console.log("Flying car landed");
  }
}

Interface Merging

TypeScript allows interfaces with the same name to be automatically merged, combining their properties.

interface Window {
  title: string;
}

interface Window {
  isOpen: boolean;
}

interface Window {
  close(): void;
}

// All declarations are merged into one interface
const window: Window = {
  title: "My Window",
  isOpen: true,
  close() {
    this.isOpen = false;
  }
};

// This is particularly useful for extending third-party library types
interface Array<T> {
  last(): T | undefined;
}

Array.prototype.last = function<T>(this: T[]): T | undefined {
  return this[this.length - 1];
};

Advanced Interface Patterns

Conditional Properties based on other properties:

interface BaseEvent {
  type: string;
  timestamp: Date;
}

interface ClickEvent extends BaseEvent {
  type: "click";
  x: number;
  y: number;
}

interface KeyEvent extends BaseEvent {
  type: "keypress";
  key: string;
  ctrlKey: boolean;
}

type UIEvent = ClickEvent | KeyEvent;

function handleEvent(event: UIEvent) {
  switch (event.type) {
    case "click":
      console.log(`Clicked at ${event.x}, ${event.y}`);
      break;
    case "keypress":
      console.log(`Key pressed: ${event.key}`);
      break;
  }
}

Mapped Types in Interfaces:

interface Person {
  name: string;
  age: number;
  email: string;
}

// Create a partial version
type PartialPerson = Partial<Person>;

// Create a readonly version
type ReadonlyPerson = Readonly<Person>;

// Pick specific properties
type PersonContact = Pick<Person, "name" | "email">;

// Omit specific properties
type PersonWithoutEmail = Omit<Person, "email">;

Interfaces are fundamental to TypeScript development, providing a clean way to define contracts, ensure type safety, and create maintainable code. They enable clear communication between different parts of your application and make your code more self-documenting and easier to refactor.