Skip to content
Home
TypeScript Types vs Interfaces: Key Differences and Use Cases

TypeScript Types vs Interfaces: Key Differences and Use Cases

TypeScript TypeScript 7 min read 1479 words Beginner ExcellentWiki Editorial Team

One of the most common questions TypeScript developers face is whether to use type aliases or interface declarations. Both define object shapes, support generics, and compose with other types. However, they have distinct capabilities and limitations. Choosing wisely affects code readability, extensibility, and type-checking performance. Microsoft’s TypeScript team recommends using interface for public API type definitions and type for unions, intersections, and mapped types (see TypeScript Handbook: Interfaces vs Types). This guide provides a comprehensive comparison.

Declaration Merging

The most significant difference is declaration merging — the ability to add properties to an existing interface at multiple points in a codebase.

interface User {
  name: string;
---

interface User {
  age: number;
---

// User is { name: string; age: number; }

This is impossible with type. Duplicate type aliases cause a compile-time error. Declaration merging is invaluable for augmenting third-party types. For example, adding a custom property to Express’s Request object:

declare global {
  namespace Express {
    interface Request {
      user?: User;
    }
  }
---

Libraries like React, Vue, and Passport.js rely on declaration merging to let consumers extend their types without modifying node_modules.

Module Augmentation

Declaration merging extends to module augmentation — adding types to modules you do not own. This is critical when extending third-party libraries:

// types/react-router-dom.d.ts
import 'react-router-dom';

declare module 'react-router-dom' {
  interface RouteObject {
    meta?: {
      title: string;
      requiresAuth: boolean;
    };
  }
---

Module augmentation allows you to extend library types without forking or monkey-patching. It works because interfaces can be reopened and extended, while type aliases cannot.

Type Aliases with Union and Intersection Types

Type aliases excel at composing types via unions (|) and intersections (&). Interfaces cannot express union types at all, and intersection via extends differs subtly from &.

type Status = 'active' | 'inactive' | 'pending';
type Result = Success | Error;
type AdminUser = User & { role: 'admin' };

Union types model finite state sets, API response variants, and configuration options. Intersection types combine multiple independent types without requiring one to extend the other. For example, merging two imported types from different libraries:

import { Serializable } from './serializer';
import { Validatable } from './validator';
type PersistedModel = Serializable & Validatable;

Interfaces can approximate intersections with extends, but only when you control at least one of the types. If both come from external libraries, & is the only option.

Interface Extension vs Type Intersection

Interfaces use extends to inherit from other types. The semantics are similar to intersection but differ in several edge cases:

interface A { a: string }
interface B extends A { b: number }
// B is { a: string; b: number; }

type C = { a: string } & { b: number };
// C is { a: string; b: number; }

Key differences:

  • Conflicting properties: If two interfaces extend a base with the same property but different types, extends produces an error. Intersection types (&) resolve to never for the conflicting property, which can mask bugs.
  • Multiple inheritance: Interfaces can extend multiple types: interface C extends A, B {}. Type aliases achieve the same with type C = A & B.
  • Computed properties: Interfaces cannot extend computed or mapped types. Type aliases can intersect with them freely.

Mapped Types and Utility Transformations

Type aliases are required for mapped types — transformations that iterate over a type’s keys:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };

Interfaces cannot express mapped types directly. The built-in utility types (Partial, Required, Pick, Omit, Record) are all defined as type aliases using mapped type syntax. If your use case involves deriving one type from another, type is the correct choice.

Template Literal Types

Type aliases also support template literal types — another feature interfaces cannot replicate:

type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'

type EventHandler<K extends EventName> = (event: K) => void;

type Handlers = {
  [K in EventName as `on${Capitalize<K>}`]: EventHandler<K>;
---;

Template literal types are used extensively in libraries like tRPC and Prisma for generating typed method names from schema strings. This capability is exclusive to type aliases.

Performance and Compiler Behavior

In large codebases, the performance difference between type and interface matters. TypeScript’s type checker caches interface declarations more aggressively than type aliases. According to the TypeScript compiler performance documentation, structurally identical interfaces are compared more efficiently than equivalent type aliases, especially when declaration merging is involved.

Benchmarks from the TypeScript team show that:

  • Interface extension chains compile faster than deeply nested intersection types.
  • Type aliases with recursive references (e.g., type Json = string | number | boolean | null | Json[] | { [key: string]: Json }) can slow down the checker.
  • Declaration merging of interfaces does not degrade performance because the compiler processes merged declarations as a single result.

For most applications, the difference is negligible. At scale (100K+ lines), preferring interface for object shapes can yield measurably faster type-checking.

Display Differences in IDE Tooltips

An often-overlooked difference is how types are displayed in IDE tooltips. Interfaces show their declared name: ButtonProps. Type aliases with intersections show the full expansion: { label: string } & { onClick: () => void } & WithChildren. For complex types, this makes interface-based tooltips significantly more readable. Developers who prioritize developer experience (especially in library code) often prefer interfaces for exactly this reason.

Practical Examples: Choosing in Real Code

Example: React Component Props

For a component library where consumers might need to extend props, use interface:

// Button.tsx
export interface ButtonProps {
  label: string;
  variant: 'primary' | 'secondary';
  disabled?: boolean;
---

export function Button(props: ButtonProps) { /* ... */ }

Consumers can extend via declaration merging:

declare module './Button' {
  interface ButtonProps {
    icon?: React.ReactNode;
  }
---

Example: API Response Types

For API types that involve unions and transformations, use type:

type ApiResponse<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

type UserResponse = ApiResponse<User>;

This cannot be expressed with interface at all.

Example: Utility Type Transformations

When deriving types from other types, type is required:

type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
---;

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
---;

Example: API State Modeling

For a fetch-state type that must work as a union, type is required because interfaces cannot express unions:

type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

This discriminated union provides exhaustive type-checking in all consumers. An interface cannot model this — each status variant must be a separate type.

When to Use Each

Use interface when:

  • Defining object shapes for public APIs or library exports
  • Augmenting types via declaration merging (e.g., Express middleware, global augmentation)
  • Writing class implementations (implements works with both but interfaces are idiomatic)
  • Modeling inheritance hierarchies where one shape extends another

Use type when:

  • Defining union types (string | number)
  • Composing types via intersection (&)
  • Creating mapped or conditional types
  • Defining tuples with labeled elements
  • Using primitive type aliases (e.g., type UserId = string)
  • Working with computed/mapped properties

FAQ

Can a type alias extend an interface?
Yes: type B = A & { b: number } where A is an interface. The intersection composes them seamlessly.

Can an interface extend a type alias?
Yes, if the type alias resolves to an object type: interface B extends A { b: number }. However, if A is a union type, extends fails because interfaces cannot extend unions.

Does declaration merging work with type aliases?
No. Multiple type declarations with the same name cause a duplicate identifier error.

Which is better for React component props?
The React ecosystem has historically favored interface for props (e.g., interface Props { ... }), but both work. The TypeScript team’s React guide uses both interchangeably. Choose based on whether you need declaration merging.

Why does TypeScript have both when they overlap so much?
Interfaces and types serve different design goals. Interfaces model the classical OOP concept of a contract that can be extended and merged. Types model mathematical set relationships — unions, intersections, and transformations. The overlap exists because object shapes are the most common type in any codebase.

How do I choose when both would work?
Follow the TypeScript team’s guideline: prefer interface for public-facing object types and type for transformations, unions, and complex compositions. Consistency across your codebase matters more than the specific choice. If your team’s linter enforces one over the other, follow the linter unless there is a compelling reason to deviate.

Internal Links

References

Section: TypeScript 1479 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top