Skip to lesson

learningtypescript.org / intermediate / 06-interfaces-and-types · lesson 6 of 25

TL;DR

Master TypeScript interfaces and type aliases. Learn extending, intersection types, declaration merging, and when to use each.

Key concepts

  • TypeScript interfaces
  • type aliases TypeScript
  • interface vs type
  • TypeScript intersection types

Interfaces and Types

TypeScript gives you two powerful tools for defining data shapes: interfaces and type aliases. Understanding when to use each is essential for clean, maintainable code.

Interfaces vs Type Aliases

Both can describe object shapes, but they differ in important ways.

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

// Type alias
type Product = {
  id: number;
  name: string;
  price: number;
};

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
const product: Product = { id: 101, name: "Keyboard", price: 79.99 };

console.log(`User: ${user.name} (${user.email})`);
console.log(`Product: ${product.name} - $${product.price}`);

// Type aliases can represent primitives, unions, and tuples
type ID = string | number;
type Coordinate = [number, number];
type Status = "active" | "inactive" | "pending";

const userId: ID = "abc-123";
const point: Coordinate = [10, 20];
const status: Status = "active";

console.log(`ID: ${userId}, Point: (${point[0]}, ${point[1]}), Status: ${status}`);

Extending Interfaces

Interfaces can extend other interfaces to build complex types from simpler ones.

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

interface Pet extends Animal {
  owner: string;
  vaccinated: boolean;
}

interface Dog extends Pet {
  breed: string;
  tricks: string[];
}

const myDog: Dog = {
  name: "Max",
  age: 3,
  owner: "Alice",
  vaccinated: true,
  breed: "Golden Retriever",
  tricks: ["sit", "shake", "roll over"]
};

console.log(`${myDog.name} is a ${myDog.breed}`);
console.log(`Owner: ${myDog.owner}, Age: ${myDog.age}`);
console.log(`Tricks: ${myDog.tricks.join(", ")}`);

// Extending multiple interfaces
interface Serializable {
  toJSON(): string;
}

interface Printable {
  display(): string;
}

interface Document extends Serializable, Printable {
  title: string;
  content: string;
}

const doc: Document = {
  title: "Meeting Notes",
  content: "Discussed quarterly goals.",
  toJSON() {
    return JSON.stringify({ title: this.title, content: this.content });
  },
  display() {
    return `${this.title}: ${this.content}`;
  }
};

console.log(`\nDocument: ${doc.display()}`);
console.log(`JSON: ${doc.toJSON()}`);

Intersection Types

Type aliases use & to combine types, similar to interface extension.

type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };

// Combine with intersection
type Person = HasName & HasAge & HasEmail;

const person: Person = {
  name: "Bob",
  age: 28,
  email: "bob@example.com"
};

console.log(`${person.name}, age ${person.age}, email: ${person.email}`);

// Intersection with inline types
type Employee = Person & {
  department: string;
  salary: number;
};

const employee: Employee = {
  name: "Carol",
  age: 35,
  email: "carol@company.com",
  department: "Engineering",
  salary: 95000
};

console.log(`\n${employee.name} works in ${employee.department}`);
console.log(`Salary: $${employee.salary}`);

// Generic response wrapper
type ApiResponse<T> = {
  data: T;
  status: number;
  timestamp: string;
};

type UserData = { id: number; username: string };
type UserResponse = ApiResponse<UserData>;

const response: UserResponse = {
  data: { id: 1, username: "alice" },
  status: 200,
  timestamp: new Date().toISOString()
};

console.log(`\nAPI Response: status ${response.status}`);
console.log(`User: ${response.data.username}`);

Declaration Merging

Interfaces support declaration merging — declaring the same interface name twice merges them. Type aliases cannot do this.

// Declaration merging
interface Config {
  apiUrl: string;
  timeout: number;
}

interface Config {
  retryCount: number;
  debug: boolean;
}

// Merged Config has all four properties
const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retryCount: 3,
  debug: false
};

console.log(`API URL: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}ms`);
console.log(`Retries: ${config.retryCount}`);
console.log(`Debug: ${config.debug}`);

Optional and Readonly Properties

interface UserProfile {
  readonly id: number;
  name: string;
  email: string;
  bio?: string;
  avatar?: string;
  readonly createdAt: string;
}

const profile: UserProfile = {
  id: 1,
  name: "Diana",
  email: "diana@example.com",
  bio: "TypeScript enthusiast",
  createdAt: "2024-01-15"
};

profile.name = "Diana Smith";
// profile.id = 2; // Error: Cannot assign to read-only property

console.log(`ID: ${profile.id} (readonly)`);
console.log(`Name: ${profile.name}`);
console.log(`Bio: ${profile.bio}`);
console.log(`Avatar: ${profile.avatar ?? "not set"}`);

// Index signatures for dynamic keys
interface StringMap {
  [key: string]: string;
}

const headers: StringMap = {
  "Content-Type": "application/json",
  "Authorization": "Bearer token123"
};

Object.entries(headers).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});

Try It Yourself

// Build a vehicle type hierarchy
interface Vehicle {
  make: string;
  model: string;
  year: number;
}

interface ElectricVehicle extends Vehicle {
  batteryCapacity: number;
  range: number;
}

interface GasVehicle extends Vehicle {
  fuelType: "regular" | "premium" | "diesel";
  mpg: number;
}

const tesla: ElectricVehicle = {
  make: "Tesla", model: "Model 3", year: 2024,
  batteryCapacity: 75, range: 358
};

const civic: GasVehicle = {
  make: "Honda", model: "Civic", year: 2024,
  fuelType: "regular", mpg: 36
};

console.log(`EV: ${tesla.year} ${tesla.make} ${tesla.model} - ${tesla.range} mi range`);
console.log(`Gas: ${civic.year} ${civic.make} ${civic.model} - ${civic.mpg} MPG`);

// Use intersection types
type Timestamps = { createdAt: string; updatedAt: string };
type SoftDeletable = { deletedAt?: string; isDeleted: boolean };

type BlogPost = {
  id: number;
  title: string;
  author: string;
} & Timestamps & SoftDeletable;

const post: BlogPost = {
  id: 1, title: "Understanding Types", author: "Alice",
  createdAt: "2024-01-15", updatedAt: "2024-01-16", isDeleted: false
};

console.log(`\n"${post.title}" by ${post.author}`);
console.log(`Created: ${post.createdAt}, Deleted: ${post.isDeleted}`);

Key Takeaways

  • Interfaces define object shapes and support declaration merging
  • Type aliases can represent any type: objects, primitives, unions, tuples, intersections
  • Use extends for interface hierarchies and & for type intersections
  • Declaration merging is unique to interfaces — useful for extending third-party types
  • Both support optional (?) and readonly modifiers
  • Prefer interfaces for public APIs, type aliases for unions and complex compositions

Pro Tip: Start with interfaces for object shapes that might be extended or implemented by classes. Use type aliases for unions, intersections, and computed types. When in doubt, start with an interface — you can always switch later.

Next Steps

You've seen how interfaces and type aliases define fixed shapes. But what if you want a single function or interface to work with many different types without losing type safety? That's where generics come in.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.