Back to blog

TypeScript Interface vs Type Alias: Key Differences and Extension Rules

When writing type-safe code in TypeScript, you define data models using two keywords: interface and type (Type Alias).

For many basic object representations, they look interchangeable:

// Interface syntax
interface User {
  name: string;
}

// Type Alias syntax
type UserType = {
  name: string;
};

However, they possess different capabilities regarding unions, inheritance mechanics, declaration merging, and compilation performance.

In this guide, we will analyze the technical differences between interfaces and type aliases, explain declaration merging, and establish coding guidelines.

1. Declaration Merging (Interface Only)

This represents the most critical functional difference between the two declarations:

  • Interfaces support Declaration Merging: If you declare two interfaces with the exact same name in the same scope, TypeScript automatically merges their properties into a single interface.
interface Window {
  title: string;
}

interface Window {
  width: number;
}

// Resulting interface has both properties: title and width
const myWindow: Window = {
  title: "Main",
  width: 1024,
};

This feature is essential for extending global variables (like adding properties to the Express Request object) or overriding third-party npm library configurations.

  • Types do not support merging: If you attempt to declare two type aliases with the same name, the TypeScript compiler immediately throws a duplicate identifier error.

2. Dynamic Types: Unions and Intersections (Type Only)

Type aliases offer greater flexibility regarding the types of data they can represent.

An interface is strictly limited to defining object shapes, functions, and class declarations. It cannot represent primitive values or complex compound types.

A type alias can define anything, including:

  • Union Types:
type Status = 'pending' | 'resolved' | 'rejected';
  • Tuple Types:
type Coordinates = [number, number];
  • Mapped Types:
type ReadOnlyUser = { readonly [K in keyof User]: User[K] };

3. Extending Types: extends vs. Intersections (&)

Both declarations support inheritance, but they use different syntaxes and compile engines:

  • Interfaces use extends:
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}
  • Types use Intersections (&):
type AnimalType = {
  name: string;
};

type DogType = AnimalType & {
  breed: string;
};

The Performance Advantage of extends

TypeScript's compiler is optimized to handle extends on interfaces. The compiler caches interface hierarchies internally to prevent recalculating property checks.

In contrast, type intersections (&) force the compiler to recursively flatten and merge property structures, which can slow down compile times in large enterprise codebases containing thousands of types.

Summary Comparison Matrix

Capability Interface Type Alias
Object shape declarations Yes Yes
Declaration Merging Yes (automatic merge) No (duplicate identifier error)
Union Types support No Yes
Primitives, Tuples representation No Yes
Extension Syntax extends Intersection (&)
Compiler Performance Fast (cached checks) Moderately slower (flatten check)

Style Guidelines: Which to Use?

To maintain a clean codebase, follow these rule priorities:

  1. Use interface by default for object shapes: Use interfaces for component props, API request/response structures, and database models. It keeps compilation fast and allows future declaration expansions.
  2. Use type when you need unions, tuples, or utilities: Mandatory for defining action states, mapping types, or aliasing primitive values.
  3. Use interface when writing libraries: If you publish npm modules, expose public settings as interfaces so developers can extend them using declaration merging.

Conclusion

Understanding the difference between TypeScript's interface and type is essential for structuring clean code. Interfaces are optimized objects designed to be extended and merged natively. Type aliases are flexible type containers designed to handle complex unions, tuples, and custom utility wrappers. By using interfaces for standard objects and types for complex logic, you optimize compiler speeds and code readability.