Interfaces and Type Aliases in TypeScript

Published Updated

TypeScript gives you two ways to name a shape, and the internet has turned the choice into a bigger argument than it deserves. For most object shapes the two forms are interchangeable. The honest version of this guide is short: learn the few real differences, pick a default, and stay consistent.

This is the second guide in the types topic. It assumes you can read the object type syntax from the first types to learn.

The Two Forms Side by Side

Here is the same shape written both ways. Code that uses Product cannot tell which form declared it:

interface Product {
  id: string;
  name: string;
  priceCents: number;
}

type ProductAlias = {
  id: string;
  name: string;
  priceCents: number;
};

const book: Product = { id: "p1", name: "Refactoring", priceCents: 4200 };
const same: ProductAlias = book; // assignable both ways

console.log(same.name);
// prints: Refactoring

TypeScript compares types by structure, not by name. Because both declarations describe the same structure, values flow freely between them, and both are erased at compilation. For a plain bag of properties, nothing observable changes when you swap one form for the other.

What Only a Type Alias Can Name

The first real difference: an interface can only describe object shapes, including callable and indexable ones. A type alias can name anything the type system can express:

type ProductId = string;

type Pair = [x: number, y: number];

type CheckoutState =
  | { status: "idle" }
  | { status: "submitting"; cartId: string }
  | { status: "failed"; message: string };

None of these three have an interface equivalent. A primitive alias, a tuple, and a union are all outside what interface can declare. The moment a shape needs to be "one of several alternatives", you are writing a type alias, and the unions and narrowing guide takes over from there.

Extending a Shape

Both forms can build on an existing shape. Interfaces use extends; aliases use an intersection with &:

interface DiscountedProduct extends Product {
  discountPercent: number;
}

type DiscountedProductAlias = Product & {
  discountPercent: number;
};

They look equivalent, and usually are. The difference appears when the added properties conflict with the base. An extends clause that redeclares priceCents as a string is an error on the interface declaration itself, pointing at the exact line.

An intersection accepts the conflict silently. A clash like string and number produces a property of type never, which errors at construction or on an invalid operation rather than on every read. Conflicting literal discriminants go further and collapse the whole intersection to never.

That early, well-placed error is the strongest practical argument for extends when you are layering object shapes. It moves the failure to the declaration, where the mistake actually lives.

Declaration Merging

The second real difference is stranger: two interface declarations with the same name in the same scope merge into one combined shape, provided duplicated non-function members have identical types. Incompatible duplicates are an error, and duplicate methods stack as overloads with their own ordering rules. A duplicate type alias is simply an error:

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

const prefs: Settings = { theme: "dark", fontSize: 14 };

console.log(prefs.fontSize);
// prints: 14

// with a type alias instead:
// error TS2300: Duplicate identifier 'Settings'.

Inside one file, merging mostly looks like a footgun, and treating a duplicate as an error, as aliases do, is arguably saner. Accidentally declaring the same interface twice produces no warning at all, so long as the duplicated members agree in type.

Across files, merging is a feature the ecosystem leans on, though same-named interfaces in separate modules do not merge on their own. Applications augment framework types through module augmentation, a declare module block targeting the named export, or declare global for ambient types. Default exports cannot be augmented this way.

If you publish types that others should extend this way, that alone decides the question: expose an augmentable named interface, or a class whose instance side a merging interface can extend.

Which to Default To

Convention differs by team, and both defaults are defensible. The TypeScript documentation suggests using interface until you need features only aliases have, largely on the strength of the better extends errors.

My own habit runs the other way: type aliases for state models, unions, and everything function-shaped, interfaces for public object contracts that might be extended or implemented. Either policy works. What does not work is a codebase where each file invents its own religion, because then every reader stops to wonder whether the difference in form encodes a difference in intent.

Whichever default you pick, the escape hatches stay the same. Reach for an alias when the shape is a union, tuple, or primitive, since interfaces cannot express those. Reach for an interface when external code must merge into the shape.

Pitfalls and Debugging

Duplicate identifier 'X'. If X is a type alias, you declared it twice, and one declaration must go. Check imports too: a local alias colliding with an imported name raises the related TS2440, Import declaration conflicts with local declaration, and renaming the import is usually the cleaner fix.

A property has become never after an intersection. Two intersected shapes declare the same property with incompatible types, and no value can satisfy both. The intersection itself never warned you. Find the conflicting property, fix one side, or switch to extends so the conflict errors at the declaration next time.

An interface has properties nobody declared. Almost always declaration merging: another declaration of the same name, often in a different file or a .d.ts, is contributing members. Editor tooling that jumps to a type's definitions will list every merged declaration site, which is the fastest way to find the contributor.

Interface 'X' incorrectly extends interface 'Y'. This is the good error, at the declaration line where the incompatible override lives. Compare the redeclared property's type with the base; the fix is either aligning the types or renaming the new property rather than silencing the error with a cast.

Frequently Asked Questions

Are interfaces faster than type aliases?

Not in shipped code, because both are erased at compilation and produce zero runtime output. Compiler performance differences exist in very large codebases, where interfaces can cache better than complex intersections, but for application-scale projects the choice is about expressiveness and convention, not speed.

Can a class implement a type alias?

Mostly yes. implements works with aliases of object types and intersections whose members are statically known, but a normal class cannot implement a callable alias, and unresolved conditional types are rejected. It also cannot implement a union, because a class must be one concrete shape rather than one of several alternatives.

Can you convert between the two forms later?

Usually yes, mechanically. An interface of properties rewrites directly as a type alias and back. The exceptions are aliases that use unions, mapped types, or conditional types, which have no interface equivalent, and interfaces that other code merges into, which an alias would break.

Why do some libraries insist on interfaces?

Because their users extend them. A published interface can be merged from outside through module augmentation, which is how projects add custom fields to framework types such as request objects or theme definitions. Library authors who want that extension point expose a named interface, or a class an interface can merge with, since aliases cannot merge.

Sources

  1. [1]
    Everyday Types
    (typescriptlang.org)
  2. [2]
    Object Types
    (typescriptlang.org)
  3. [3]
    Declaration Merging
    (typescriptlang.org)