typescript · level 2

Interfaces vs Type Aliases

Same job 80% of the time. The 20% where the answer is forced.

150 XP

Interfaces vs Type Aliases

The most asked TypeScript question after "what's any" is "interface or type?" The answer is "it depends on three forced cases — and beyond that, taste." Here's how to know which case you're in.

What they have in common

For ordinary object shapes, both work and produce identical types:

interface User { id: number; name: string }
type     User = { id: number; name: string };

Both can:

  • describe object shapes
  • be extended (extends for interface, & intersection for type)
  • include optional properties (?), readonly modifiers, index signatures, and call signatures
  • be implemented by classes via implements

For 80% of code, swapping one for the other compiles cleanly. The 20% is what matters.

Three forced cases for type

1. Unions

interface is for object shapes. A union of two object shapes is not an object shape:

type Result = "ok" | "error";
type Shape = Circle | Square | Triangle;
// interface Result = ... ❌ syntax error

The same goes for null | string, 1 | 2 | 3, every literal union — type only.

2. Primitives

interface UserId = string is not legal syntax. If you want a name for a primitive, you need an alias:

type UserId = string;
type Pixels = number;
type IsoDate = `${number}-${number}-${number}`;  // template-literal type

3. Tuples

Tuples are positional types, not property bags:

type Coord = [number, number];
type Pair<A, B> = [A, B];

interface can mimic this with index signatures, but the result is uglier and loses positional inference.

4. Mapped & conditional types

Anything computed from another type — Partial<T>, Pick<T, K>, T extends U ? X : Y — needs type:

type Optional<T> = { [K in keyof T]?: T[K] };
type Returned<T> = T extends () => infer R ? R : never;

Two forced cases for interface

1. Declaration merging

You can declare an interface twice in the same scope and TypeScript merges the declarations:

interface Window {
  myAppConfig: { apiUrl: string };
}
// Combined with the lib.dom.d.ts declaration of Window —
// `window.myAppConfig` is now typed.

This is the only way to extend types defined in third-party libraries (or lib.dom.d.ts) without forking the upstream. If you've ever added a property to globalThis, augmented Express.Request, or extended JQuery, you've used declaration merging — which forces interface.

type Window = { myAppConfig: { apiUrl: string } };
// ❌ Duplicate identifier 'Window'

2. Public library surfaces

By extension, library authors should publish object types as interfaces. Doing so lets consumers extend the type by re-declaring it. If you publish type User = ..., downstream code is locked out — they have to wrap your type to extend it.

What the official guide says

The TypeScript handbook recommends: "Use interface until you need to use features from type." That's the right default. The two reasons:

  1. Better error messages. TypeScript displays interface names in errors; type aliases get inlined into the offending expression. With deep types this is a real readability difference.
  2. extends is symmetric and obvious. interface Admin extends User reads like English. type Admin = User & { role: "admin" } is fine but more abstract.

Common confusions

"Interface is faster to compile than type." Used to be true in some narrow cases. Modern TS has closed the gap; don't optimise on this axis.

"Type can do everything interface can." Almost — except declaration merging. That single feature is non-trivial to lose if your type ever ships in .d.ts.

"Type unions are safer." Neither is safer. They express different things: union types ARE values that can be one of N shapes; an interface IS one shape. Pick the meaning you want.

A decision tree

Is it a union, primitive, tuple, or computed type?
  → type

Will external code extend this via declaration merging?
  → interface

Is it an object shape used internally?
  → either; prefer interface for consistency

What teams usually settle on

A common convention that survives style-guide debates:

  • interface for every object shape. Forces consistency, plays well with tooling, gives nicer error messages.
  • type for unions, mapped types, function aliases, and anywhere else interface simply can't go.
  • ESLint rule @typescript-eslint/consistent-type-definitions enforces this.

That keeps the question off the PR review surface — which is mostly the point.

A worked example

// type for the union and computed
type Status = "active" | "archived" | "deleted";
type StatusColors = { [S in Status]: string };

// interface for shapes (incl. extension)
interface User {
  id: number;
  name: string;
  status: Status;
}
interface Admin extends User {
  permissions: readonly string[];
}

// type for the function (cleaner reads than interface call-signature)
type Save = (u: User) => Promise<void>;

Three different tools, each used where it's clearly best. That's the whole game.

Tools in the wild

3 tools