March 26, 20265 min read

TypeScript Interfaces vs Type Aliases: The Real Differences

What's actually different between interface and type in TypeScript, when the choice matters, when it doesn't, and what the community convention is.

typescript interfaces types javascript
Ad 336x280

This question comes up in every TypeScript codebase eventually. Someone writes an interface, someone else writes a type, and then there's a code review comment asking why. The honest answer is that 95% of the time, either one works fine. But there are real differences, and knowing them saves you from the 5% where it actually matters.

The Quick Version

Both can describe the shape of an object:

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

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

For this case, they're interchangeable. You can use either with classes, function parameters, generics -- all of it works the same.

Where They Diverge

Declaration Merging (Interfaces Only)

If you declare the same interface twice, TypeScript merges them:

interface Window {
  myCustomProp: string;
}

// This merges with the built-in Window interface
// Now window.myCustomProp is typed

Type aliases can't do this. Declaring the same type name twice is an error:

type Window = { myCustomProp: string }; // ERROR: Duplicate identifier

Declaration merging matters for library authors who want consumers to extend types. It's why most of the built-in DOM types and library definitions use interfaces. But in application code, you almost never want two declarations of the same name merging silently -- that's more confusing than helpful.

Unions and Intersections (Type Aliases)

Type aliases can express things interfaces can't:

type Status = 'loading' | 'success' | 'error';

type StringOrNumber = string | number;

type Result<T> = { success: true; data: T } | { success: false; error: string };

An interface can't be a union. It has to describe a single object shape. If you need a union, discriminated union, or any non-object type, you need type.

Extending vs Intersection

Interfaces extend other interfaces:

interface Animal {
  name: string;
}

interface Dog extends Animal {
breed: string;
}

Type aliases use intersection:

type Animal = {
  name: string;
};

type Dog = Animal & {
breed: string;
};

Both produce the same result. There's a subtle difference though: extends checks for conflicts at declaration time, while & silently creates never types for conflicting properties. Consider:

interface A {
  x: string;
}

// This is an error -- x can't be both string and number
interface B extends A {
x: number;
}

// This compiles but x becomes 'string & number' which is 'never'
type C = A & { x: number };

The interface version catches the mistake immediately. The intersection silently creates an unusable type. Point to interfaces here.

Mapped Types (Type Aliases Only)

You can't use mapped types with interfaces:

type Flags = {
  [K in 'darkMode' | 'notifications' | 'beta']: boolean;
};
// { darkMode: boolean; notifications: boolean; beta: boolean }

No interface equivalent exists. Utility types like Pick, Omit, Partial, Record -- they all produce type aliases, not interfaces.

Computed Properties

Similarly, type aliases support template literal types and other computed patterns:

type EventName = 'click' | 'scroll' | 'resize';
type EventHandlers = {
  [K in EventName as on${Capitalize<K>}]: () => void;
};
// { onClick: () => void; onScroll: () => void; onResize: () => void }

Interfaces can't do this.

Performance

The TypeScript team has noted that interfaces can be slightly faster for type checking in large codebases because they create a named reference that gets cached, while complex intersections need to be flattened every time. In practice, you won't notice this unless your project has thousands of types. But it's worth knowing if you're maintaining a large shared library.

When the Choice Actually Matters

ScenarioUse
Union types, discriminated unionstype (interfaces can't)
Mapped types, utility typestype (interfaces can't)
Primitive aliases (type ID = string)type (interfaces can't)
Library definitions that consumers extendinterface (declaration merging)
Class implementsEither (both work)
Object shapes in application codeEither (convention preference)
Catching property conflicts earlyinterface extends (stricter checking)

The Community Convention

Most teams settle on this pattern:

  • Interfaces for object shapes -- things with properties and methods
  • Type aliases for everything else -- unions, intersections, utility types, primitives, tuples
This isn't a technical requirement. It's a convention that creates consistency. The React ecosystem mostly uses this pattern. The Angular ecosystem leans heavier on interfaces.

Some teams just use type for everything. That works too. The worst thing you can do is mix randomly with no rationale -- pick a convention and stick with it.

My Honest Take

If you're starting a new project, use interfaces for your data models and type aliases for everything else. If you're joining an existing project, follow whatever they're already doing. If someone argues passionately about this in a code review, they're spending energy on the wrong thing.

The real TypeScript skill isn't choosing between interface and type -- it's learning the type system deeply enough to model your domain correctly. Generics, discriminated unions, narrowing, conditional types -- those are the things worth investing time in.

Practice both patterns with interactive TypeScript exercises on CodeUp -- the best way to internalize this stuff is to write it yourself and see what the compiler tells you.

Ad 728x90