Where They Look the Same

For the most common use case — describing an object shape — both are syntactically close and produce identical type-checking behaviour:

interface
interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
}
type alias
type User = {
  id: string;
  name: string;
  email: string;
  age?: number;
}

Both support optional properties (?), readonly modifiers, method signatures, generics, and extension. For object shapes in application code, pick one and be consistent.

Difference 1: Declaration Merging

interface supports declaration merging — declaring the same interface name twice merges the definitions. type does not allow redeclaration:

interface Window {
  myCustomProp: string;
}
// Now window.myCustomProp is typed ✅
// This is how DefinitelyTyped augments browser globals

type Window = { myCustomProp: string }
// Error: Duplicate identifier 'Window'. ❌

Declaration merging is essential for module augmentation — extending third-party library types, global DOM augmentation, or adding properties to Express's Request type. This capability is exclusive to interface.

Difference 2: Union and Intersection Types

type can represent union types, intersection types, and mapped types directly. interface cannot:

// Union — only possible with type
type Status = "pending" | "active" | "archived";
type ID = string | number;
type Result<T> = { data: T } | { error: string };

// Intersection (type alias)
type AdminUser = User & Admin;

// interface can extend (close equivalent)
interface AdminUser extends User, Admin {}
// But interface extends fails for union members

Difference 3: Computed and Mapped Types

Computed property keys and mapped types are exclusively available in type:

// Mapped type — type only
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Conditional type — type only
type NonNullable<T> = T extends null | undefined ? never : T;

TypeScript's built-in utility types (Partial<T>, Required<T>, Pick<T,K>, Omit<T,K>) are all implemented as mapped type aliases for this reason.

Difference 4: Extending Syntax

interface extends
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}
type intersection
type Animal = { name: string };

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

Both achieve similar results. The key difference: interface extends will error if the extended type has incompatible property types. type & intersection will silently produce never for the conflicting property — which can cause hard-to-debug errors.

Difference 5: Error Message Quality

Interfaces produce cleaner error messages. TypeScript inlines type aliases in errors, which can make complex types extremely verbose in the output. For large teams, this is a real ergonomics consideration when choosing for domain model types.

The Definitive Decision Table

Capabilityinterfacetype
Object shape definitionYesYes
Optional / readonly propertiesYesYes
GenericsYesYes
Extension / compositionYes (extends)Yes (&)
Declaration mergingYesNo
Module augmentationYesNo
Union typesNoYes
Intersection typesNo (use extends)Yes
Mapped typesNoYes
Conditional typesNoYes
Tuple typesNoYes
Primitive aliasesNoYes

Practical Recommendation

✅ Guideline
  • Use interface for: domain model types (User, Order, Product), class contracts (implements), public library APIs that consumers may need to extend via declaration merging.
  • Use type for: union types, intersection compositions of multiple types, utility type definitions, any type that is not purely an object shape (primitives, tuples, functions).
  • Pick one for your "plain object shape" standard and document it in your team's style guide. Consistency matters more than the specific choice.

Common Migration Pitfalls

Converting existing code between the two is usually mechanical, but two failure modes catch people every time:

Pitfall 1: "Duplicate identifier" after converting interface → type

This means something else in the codebase — often a .d.ts file from a dependency, or your own code in a different file — was relying on declaration merging with that exact name:

// api-types.ts
interface Request {
  userId: string;
}

// middleware.ts — adds a property via merging
interface Request {
  sessionToken: string;
}
// Request now has both properties — this only works because both are `interface`
// Converting either one to `type` breaks the merge with a duplicate-identifier error ❌

Before converting an interface, search the whole codebase (and check `.d.ts` files pulled in from dependencies) for every other declaration of that name.

Pitfall 2: Generic constraints behave differently after switching extends to &

Swapping interface Dog extends Animal for type Dog = Animal & { ... } looks equivalent, but if a property's type is incompatible between the two, the outcomes diverge:

interface Base { id: string; }
interface Broken extends Base { id: number; }
// Error at declaration: interface catches the conflict immediately ✅

type Base2 = { id: string; };
type Broken2 = Base2 & { id: number; };
// No error here — id silently collapses to `never`.
// The error surfaces later, somewhere confusing, when you try to assign an id ❌

If you're migrating an inheritance-heavy hierarchy from interface to type, this is the single most common source of "it compiled but broke downstream" bugs. Prefer keeping inheritance-heavy domain hierarchies as interface for this reason alone.

Frequently Asked Questions

Is TypeScript interface faster than type, or vice versa?

Neither has a runtime performance difference — both are erased completely during compilation, so there's zero cost in the browser or Node either way. At the type-checking level (your editor and tsc), very large union or mapped types can make the compiler work harder than an equivalent interface hierarchy would, but for everyday object-shape definitions the difference is not measurable.

Does the TypeScript team officially recommend interface over type?

The TypeScript Handbook says to prefer interface until you need a feature only type provides. That guidance is really about public API design: interfaces support declaration merging, so a library's consumers can augment its types later. For internal application code where nothing outside your codebase needs to extend the shape, the choice matters far less than the team consistently picking one.

Can I automatically convert a TypeScript interface to a type alias?

For a plain object shape, yes — interface X { ... } becomes type X = { ... } with no behavior change. It breaks the moment the interface relies on declaration merging elsewhere in the codebase (see Pitfall 1 above), since type aliases can't be redeclared. Search the codebase for every occurrence of the interface name before converting.

Why did I get a "duplicate identifier" or "does not satisfy the constraint" error after switching from interface to type?

See the Common Migration Pitfalls section above — both errors trace back to the same root cause: interface merges and validates conflicts at declaration time, while type intersections resolve silently and can surface confusing errors later, somewhere else in the code.