TypeScript Interfaces

TypeScript interfaces are a powerful way to define the structure of objects in your code. They help you create clear contracts about what shape your data should have, making your code more predictable and easier to understand.

Basic Interface Syntax

An interface is like a blueprint that describes what properties and methods an object should have:

Basic Interface

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

// Creating an object that matches the interface
const john: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
};

Optional Properties

Not all properties need to be required. Use the ? symbol to mark properties as optional:

Optional properties

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

// Both of these are valid
const laptop: Product = {
  id: 1,
  name: "Laptop",
  price: 999,
};

const phone: Product = {
  id: 2,
  name: "Phone",
  price: 699,
  description: "Latest smartphone model",
};

Readonly Properties

If you want to prevent properties from being changed after initialization, use the readonly modifier:

ReadOnly

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

const point: Point = { x: 10, y: 20 };
// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property

Function Types in Interfaces

Interfaces can also describe functions:

Function description

interface CalculateTotal {
  (price: number, quantity: number, taxRate: number): number;
}

const calculateTotal: CalculateTotal = (price, quantity, taxRate) => {
  return price * quantity * (1 + taxRate);
};

Method Definitions

You can define methods inside interfaces:

Model definition

interface Logger {
  log(message: string): void;
  error(message: string): void;
}

const consoleLogger: Logger = {
  log(message) {
    console.log(`LOG: ${message}`);
  },
  error(message) {
    console.error(`ERROR: ${message}`);
  },
};

Extending Interfaces

Interfaces can extend other interfaces to inherit their properties:

Extending interface

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

interface Employee extends Person {
  employeeId: number;
  department: string;
}

const employee: Employee = {
  name: "Alice",
  age: 30,
  employeeId: 123,
  department: "Engineering",
};

Types vs. Interfaces

TypeScript offers two ways to define object types: interface and type. While they're similar in many ways, understanding their differences helps you choose the right one for your needs.

Basic Syntax Comparison

// Interface
interface User {
  name: string;
  age: number;
}

// Type
type User = {
  name: string;
  age: number;
};

Key Differences

1. Declaration Merging

Interfaces can be defined multiple times, and they will be merged:

// Interface merging
interface User {
  name: string;
}

interface User {
  age: number;
}

// The User interface now has both name and age
const user: User = {
  name: "John",
  age: 30,
};

Types cannot be redefined or merged:

type User = {
  name: string;
};

// Error: Duplicate identifier 'User'
// type User = {
//   age: number;
// };

2. Extending

Interfaces use the extends keyword:

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

Types use the & operator (intersection):

type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
};

3. Additional Capabilities of Types

Types can do things that interfaces cannot:

// Union types
type ID = string | number;

// Literal types
type Direction = "north" | "south" | "east" | "west";

// Type aliases for primitives
type Age = number;

// Mapped types
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

When to Use Which?

Overview: Interfaces vs Types

Type vs Interface Comparison
FeatureInterfacesTypes
Primary UseObject shapes, API contractsUnions, intersections, primitives
Declaration MergingYesNo
Extends/Implements

via extends

via & (intersection)

Primitive TypesNo Yes
Union Types No Yes
Tuple Types No Yes
Can be Computed No Yes

Use Interfaces When:

  1. You're defining objects or classes that implement a contract:
interface Repository {
  findById(id: string): unknown;
  save(data: unknown): void;
}

class UserRepository implements Repository {
  // Implementation here
}
  1. You want to allow declaration merging:
// Useful when extending libraries or working with declaration files
interface Window {
  customProperty: string;
}
  1. You're working with object shapes for data:
interface User {
  id: number;
  name: string;
}

Use Types When:

  1. You need unions, intersections, or tuples:
type Result = Success | Error;

type Coordinates = [number, number];
  1. You want to use primitives or literals:
type ID = string;

type Status = "pending" | "completed" | "failed";
  1. You need to manipulate types:
type UserKeys = keyof User;

type PartialUser = Partial<User>;

Practical Examples

React Component Props

// Props interface for a Button component
interface ButtonProps {
  text: string;
  onClick: () => void;
  color?: "primary" | "secondary" | "danger";
  disabled?: boolean;
}

function Button({ text, onClick, color = "primary", disabled = false }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${color}`}
    >
      {text}
    </button>
  );
}

// Usage
<Button
  text="Click me"
  onClick={() => alert("Clicked!")}
  color="primary"
/>

Form Data Interface

interface FormData {
  username: string;
  password: string;
  rememberMe?: boolean;
}

function submitForm(data: FormData) {
  console.log("Submitting:", data);
  // API call to submit the form
}

const loginData: FormData = {
  username: "john_doe",
  password: "secure_password",
  rememberMe: true,
};

submitForm(loginData);

Best Practices

1. Use PascalCase for naming interfaces:

interface UserProfile { ... } // Good
interface userProfile { ... } // Not ideal

2. Don't use the "I" prefix:

interface User { ... } // Good
interface IUser { ... } // Not recommended

3. Keep interfaces focused on a single concept:

// Better to have two separate interfaces
interface User { ... }
interface UserPreferences { ... }

// Instead of one large interface
// interface UserWithPreferences { ... }

4. Use readonly for immutable properties:

interface Config {
  readonly apiKey: string;
  readonly serverUrl: string;
}

5. Document your interfaces with JSDoc:

/**
 * Represents a user in the system
 */
interface User {
  /** Unique identifier */
  id: number;
  /** User's full name */
  name: string;
}

Exercises

Exercise 1: Basic Interface

Create an interface for a Book with these properties:

  • title (string)
  • author (string)
  • pages (number)
  • isPublished (boolean)
  • genres (array of strings)

Then create a few book objects using this interface.

Exercise 2: Interface with Optional Properties

Create an interface for a UserProfile with these properties:

  • id (number)
  • username (string)
  • email (string)
  • bio (string, optional)
  • age (number, optional)
  • isPremium (boolean, optional)

Create a function that prints user information, with special handling for optional properties.

Exercise 3: Interfaces with Methods

Create an interface for a Calculator with these methods:

  • add(a: number, b: number): number
  • subtract(a: number, b: number): number
  • multiply(a: number, b: number): number
  • divide(a: number, b: number): number

Then implement this interface with a class.