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.