Type vs Interface

Difficulty: Intermediate

One of the most common TypeScript questions - in interviews and in practice - is when to use `type` versus `interface`. Both can describe object shapes, and for simple cases they're interchangeable. However, they have distinct capabilities that make each more appropriate in specific situations. Understanding these differences helps you make the right choice and write idiomatic TypeScript.

Interfaces have two unique capabilities: declaration merging and `extends` syntax. Declaration merging means you can declare the same interface name multiple times and TypeScript combines them automatically. This is essential for augmenting third-party library types. The `extends` syntax provides clear, explicit inheritance and catches property conflicts at the point of declaration. Interfaces also have slightly better error messages when complex object types don't match.

Type aliases have broader capabilities: they can represent unions (`A | B`), intersections (`A & B`), tuples (`[string, number]`), mapped types, conditional types, and primitive type aliases (`type ID = string`). Interfaces cannot express any of these. If you need a union type, a tuple type, a function type alias, or any computed/mapped type, you must use `type`. This makes type aliases more versatile overall.

The practical guideline used by most teams: use `interface` for defining the shapes of objects and classes (especially public APIs that others might need to extend), and use `type` for everything else - unions, intersections, utility types, function types, and any type that isn't purely an object shape. Some teams prefer a simpler rule: always use `type` unless you specifically need declaration merging. Either convention works as long as the team is consistent.

Code examples

Interface-only features

// Declaration merging - only interfaces can do this
interface Window {
  customProperty: string;
}

interface Window {
  anotherProperty: number;
}

// Both declarations merge into one interface
// Useful for extending third-party types

// extends syntax - clearer than intersection for objects
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const dog: Dog = { name: "Rex", breed: "German Shepherd" };
console.log(`${dog.name} is a ${dog.breed}`);

Declaration merging is unique to interfaces - declaring the same interface name twice merges them. The `extends` keyword provides explicit inheritance with conflict detection.

Type alias-only features

// Union types - interfaces can't do this
type Status = "loading" | "success" | "error";
type ID = string | number;

// Tuple types - interfaces can't do this
type Coordinate = [number, number];
type NameAge = [string, number];

// Mapped/utility types - interfaces can't do this
type Nullable<T> = T | null;
type ReadonlyUser = Readonly<{ name: string; age: number }>;

// Function type alias - cleaner than interface for functions
type Formatter = (value: unknown) => string;

const formatId: Formatter = (value) => `ID: ${String(value)}`;
const coord: Coordinate = [10, 20];
const status: Status = "success";

console.log(formatId(42));
console.log(`Position: ${coord[0]}, ${coord[1]}`);
console.log(`Status: ${status}`);

Type aliases can express unions, tuples, mapped types, and function types - none of which are possible with interfaces. These are the cases where `type` is required.

Practical guidelines: when to use each

// USE INTERFACE: object shapes and class contracts
interface UserProfile {
  id: number;
  name: string;
  email: string;
  bio?: string;
}

// USE TYPE: unions, computed types, and non-object types
type UserRole = "admin" | "editor" | "viewer";
type UserWithRole = UserProfile & { role: UserRole };
type UserKeys = keyof UserProfile;

function createUser(name: string, role: UserRole): UserWithRole {
  return {
    id: Math.floor(Math.random() * 1000),
    name,
    email: `${name.toLowerCase()}@example.com`,
    role
  };
}

const user = createUser("Alice", "admin");
console.log(`${user.name} (${user.role}) - ${user.email}`);

Use interfaces for object shapes (UserProfile) and type aliases for everything else: unions (UserRole), intersections (UserWithRole), and utility types (UserKeys). This is the most common convention.

Key points

Concepts covered

type vs interface, Declaration merging, Union and intersection types, When to use each