Skip to content
TypeScript ts interfaces 4 min read

Interface vs Type

“Interface or type alias?” is one of the most common TypeScript questions, and the honest answer is that for plain object shapes they are nearly interchangeable. The real differences show up at the edges: interfaces can be reopened and merged, while type aliases can express unions, intersections, mapped, and conditional types that interfaces cannot. This page compares the two head to head, shows what each can do that the other cannot, and gives a practical rule for choosing between them.

They overlap for object shapes

For describing the shape of an object, an interface and a type alias produce essentially the same type. Both support optional members, readonly members, methods, and index signatures.

interface UserI {
  id: number;
  name: string;
}

type UserT = {
  id: number;
  name: string;
};

A value of UserI is assignable to UserT and vice versa, because TypeScript compares them structurally. If your shape is a simple object, the choice is mostly stylistic.

What only interfaces can do: merging and implements

Interfaces participate in declaration merging: declaring the same interface name twice merges the members into one. This is what makes libraries augmentable. Type aliases are sealed — redeclaring one is an error.

interface Window {
  myGlobal: string;
}
interface Window {
  anotherGlobal: number;
}
// Window now has both myGlobal and anotherGlobal

type Box = { width: number };
// ❌ Error: Duplicate identifier 'Box'.
type Box = { height: number };

Both interfaces and type aliases can be implemented by classes and extended, but only an interface can be merged or augmented from another file — the basis of module augmentation for extending third-party types.

What only type aliases can do: unions, mapped, conditional

A type alias can name any type, not just an object: unions, intersections, primitives, tuples, mapped types, conditional types, and template literal types. Interfaces are restricted to object and function shapes.

type Status = "idle" | "loading" | "success" | "error"; // union
type ID = string | number;                               // union of primitives
type Pair = [number, number];                            // tuple
type Nullable<T> = T | null;                             // generic alias

// mapped type — impossible with an interface
type Optional<T> = { [K in keyof T]?: T[K] };

// conditional type — impossible with an interface
type ElementType<T> = T extends (infer U)[] ? U : T;

None of these can be written as an interface. Whenever you need a union, a mapped transformation, or a conditional, you must use a type alias.

Extension syntax compared

Both can build on other types, with slightly different syntax. Interfaces use extends; type aliases use the & intersection operator.

interface Animal { name: string }
interface Dog extends Animal { breed: string }

type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

// interfaces can even extend type aliases (if the alias is an object type)
interface Cat extends AnimalT { indoor: boolean }

Interface extends reports member conflicts at the declaration; intersection & silently resolves a conflicting property to never, which can hide bugs until use.

Side-by-side comparison

Capabilityinterfacetype alias
Object shapesYesYes
Extend / composeextends& intersection
Declaration mergingYesNo
Module augmentationYesNo
Union typesNoYes
Mapped typesNoYes
Conditional typesNoYes
Tuples & primitivesNoYes
Implemented by a classYesYes (if object shape)
Conflicts on extendError at declarationResolves to never

Performance and error messages historically favored interfaces for large extended object hierarchies, because the compiler can cache them and they appear by name in errors. The difference is small in modern TypeScript but is a real tiebreaker for big public types.

A practical rule

Default to interface for public object shapes and class contracts — it merges, extends cleanly, and gives clearer errors. Use type when you need anything an interface cannot express: unions, intersections of non-object types, tuples, mapped, conditional, or template literal types. Consistency within a codebase matters more than the marginal differences, so pick a default and document the exceptions.

// public object contract → interface
interface ApiClient {
  get(url: string): Promise<Response>;
}

// union / computed type → type alias
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };

Best Practices

  • Default to interface for object shapes and class contracts.
  • Use type for unions, intersections of non-objects, tuples, mapped, and conditional types.
  • Remember only interfaces merge and support module augmentation of external packages.
  • Use interface extends over & for object hierarchies to catch conflicts early.
  • Keep a consistent convention across the codebase rather than mixing arbitrarily.
  • Prefer interfaces for large public APIs where named errors and caching help.
  • Do not agonize over simple shapes — both work; consistency wins.
Last updated June 29, 2026
Was this helpful?