Interfaces vs Types in TypeScript

I compare TypeScript interfaces and type aliases on the differences that actually matter — naming, merging, and error messages — and give a simple decision rule.

Interfaces vs Types in TypeScript

“Should I use an interface or a type?” is the TypeScript question most likely to start a friendly war in code review. It shouldn’t. The official docs themselves land on “for the most part, you can choose based on personal preference” — with a heuristic I’ll defend at the end of this post.

But “mostly interchangeable” isn’t “identical,” and treating them that way produces confused code. The real differences are few, mechanical, and worth knowing cold: what each one can name, how each one merges, and how each one behaves when things go wrong. I’ll show those three, side by side, and then give you a decision rule you can actually apply mid-review.

Series: TypeScript, part 2 of 4. Previous: TypeScript for JavaScript Developers: Where to Start.

On this page

The 90%: they do the same job

For naming the shape of an object — the overwhelming majority of use — both tools are a coin flip:

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

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

Both work as parameter types, return types, property types. Both compose with the structural typing from part 1 — pass any object with id and name and nobody asks which declaration you used. Any argument built on “interfaces are for classes, types are for functions” folklore ends here, in the identical 90%.

The differences live in the remaining 10%. That’s where to spend attention.

What a type can name that an interface can’t

An interface declares object shapes. Full stop. It can’t rename a primitive, and it can’t express a union. Type aliases have no such ceiling:

type Status = "idle" | "loading" | "error";
type Point = [number, number];
type Id = number;

None of those three lines has an interface equivalent — there’s no object shape to declare. This single fact decides many real cases on the spot: the moment you’re naming a union (string literal states are everywhere), a tuple, or a mapped type, the decision has already been made for you. This is also why the humble type alias shows up constantly even in interface-first codebases — some names simply aren’t shapes.

Merging: the interface’s superpower

Here’s the difference with teeth. Type aliases cannot participate in declaration merging; interfaces can. Declare two same-name interfaces and TypeScript quietly combines them:

interface Invoice {
  id: number;
}

interface Invoice {
  paid: boolean;
}
// Invoice now has both id and paid

Declare a same-name type twice and you get an error, no negotiation. On purpose, this looks like a footgun — two blocks silently becoming one? In practice it powers a feature you’ve probably benefited from: module augmentation. Library types you extend (adding a property to a global or a third-party interface) work because the library declared an interface and the compiler merged your addition into it.

The merging rules also make interfaces safer when types conflict. Same-name interface properties with incompatible types raise an error; the compiler refuses the merge. Intersections — the type world’s composition tool — take the opposite route:

type A = { name: string };
type B = { name: number };

type C = A & B; // no error: name must satisfy BOTH — i.e., never

C compiles, then produces a name that must be simultaneously a string and a numberthe docs call out exactly this “unexpected results” trap. The interface path fails loudly at the merge; the intersection path fails quietly at the use site, sometimes much later. When composing types that might conflict, that contrast alone justifies a preference.

Errors and compiler work

Two smaller, real differences:

Error messages name interfaces, always. An interface’s name will appear in error messages; a type alias’s may not. Alias names usually do show up in modern TypeScript, but the guarantee only runs one direction — deep in a gnarly generic error, an interface keeps its label while an alias may dissolve into its expanded structure. When you’re debugging a 40-line error blob, a name is a breadcrumb.

extends beats & for the compiler. The docs state it plainly: using interfaces with extends can often be more performant for the compiler than type aliases with intersections. Not a reason to restructure a small project — but on a large codebase with wide type hierarchies, it’s a tiebreaker that costs nothing.

The comparison in one table

Ability interface type
Name an object shape yes yes
Name unions, tuples, primitives no yes
Combine types extends intersections (&)
Combine with conflicting props errors at the merge succeeds silently (impossible prop)
Same-name declarations merge (augmentation) error
Named in error messages always usually

Read the table top to bottom and notice the shape of the answer: type is the broader naming tool, interface is the stricter structuring tool. Neither dominates.

Which one, by situation

Your situation Reach for Why
Naming a union, tuple, or primitive type the only one that can
Public types others may augment (libraries, globals) interface declaration merging is the feature
Composing types that might conflict interface + extends loud failure over silent never-props
A default when you have no strong signal interface the docs’ own heuristic
One-off anonymous shape with no name worth giving inline type no declaration at all

The docs’ heuristic, which I promised to defend: “use interface until you need to use features from type”. It’s not that interfaces are better — it’s that the heuristic terminates. Start from interface, and the moment you hit a union or a mapped type, the language itself forces the switch. You never have to think about which to prefer, only which the current shape requires.

Pick a default and break it knowingly

The teams that fight about interfaces vs types are usually suffering from a different disease: no default at all, so every declaration re-litigates the question. The choice costs almost nothing either way. The inconsistency — three declarations of each kind in one file, chosen by mood — costs readability forever.

So take the heuristic, apply it mechanically, and break it deliberately: interface by default, type the moment a name isn’t an object shape. Write the rule down where your team argues about it, and the argument ends. The next post in this series tackles the feature that makes both tools flex: generics — and how one well-named type parameter can delete a dozen near-duplicate declarations.

Sources