Type Aliases vs Interfaces

Introduction

TypeScript offers two main ways to name types: interface for object-like contracts and type for aliases of any type shape. They overlap for plain objects, which confuses beginners. This chapter clarifies syntax differences, capabilities, and practical rules for choosing one style consistently.

Prerequisites

Defining the Same Shape Two Ways

Object shape with interface:

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

Same shape with type:

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

For many teams, these are interchangeable for simple objects. Differences appear with unions, extension, and advanced composition.

What Only type Can Express (Easily)

Unions

typescript
type Result = "success" | "error";
type Id = string | number;

Tuples

typescript
type Pair = [number, number];

Primitives and intersections combined

typescript
type NonEmptyString = string & { readonly __brand: unique symbol };

(Branded types are an advanced pattern; the point is interface cannot alias a primitive alone.)

Mapped and conditional types (preview)

typescript
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

Utility-style transformations almost always use type.

What interface Emphasizes

Declaration merging

typescript
interface Env {
  NODE_ENV: string;
}
 
interface Env {
  PORT: string;
}
 
// Env has NODE_ENV and PORT

type aliases cannot merge redeclarations—duplicate type Env names error.

extends for object inheritance

typescript
interface Animal {
  name: string;
}
 
interface Dog extends Animal {
  breed: string;
}

With type, use intersection:

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

Both work; interface + extends often reads clearer for object hierarchies.

Intersection with type

typescript
type Timestamped = {
  createdAt: string;
};
 
type Post = Timestamped & {
  id: number;
  title: string;
};

Code explanation:

  • & combines members; conflicts on the same property must be compatible
  • Large intersections can become hard to read—split with interface when it helps

implements in Classes

Both work with classes:

Side-by-Side Comparison

Featureinterfacetype
Object shapesYesYes
Extend / merge objectsextends, declaration merging&, no merging
Union / literal unionsNo (not directly)Yes
TupleNoYes
Primitive aliasNoYes
Mapped / conditional typesNoYes
Error messages on objectsOften slightly clearerGood

Style Guide: Practical Rules

  1. Object models in app/domain code — prefer interface (User, Order, ApiClient).
  2. Unions, tuples, function unions — use type (Result, Route, Handler).
  3. Composition of unions with objects — use type:
typescript
type LoadingState = { status: "loading" };
type SuccessState<T> = { status: "success"; data: T };
type ErrorState = { status: "error"; message: string };
 
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
  1. Public library API — pick one style per exported concept and document it; consistency beats dogma.
  2. Do not duplicate the same shape as both interface User and type User in one codebase.

Tip

Team Consistency Wins

TypeScript does not require one winner. Align with your linter (ESLint @typescript-eslint/consistent-type-definitions) or team guide.

Converting Between Styles

Interface to type (manual):

typescript
interface Point {
  x: number;
  y: number;
}
 
type PointAlias = Point;

Type object to interface (only for plain objects):

typescript
type PointType = { x: number; y: number };
 
// Cannot auto-convert; redefine if you need interface merging
interface PointInterface {
  x: number;
  y: number;
}

Example: Choosing in a Small Module

Code explanation:

  • PaymentMethod and PaymentResult need unions → type
  • Customer and PaymentService are object contracts → interface
  • Narrowing result.ok is a discriminated union pattern (detailed in a later chapter)

Anti-Patterns

Anti-patternWhy it hurts
type User = { ... } and interface User { ... } togetherName collision, confusion
`interface AB`
Giant type intersections instead of interface extendsHarder errors, slower editor
Using any in aliasesDefeats purpose of naming types

FAQ

Which does React use for props?

Both appear: interface Props is common; type Props = { ... } is equally valid. Function components with unions often prefer type.

Can I extend a type alias interface-style?

Use intersection: type B = A & { extra: true }. For interface, use extends.

Performance difference?

Compile-time only; no runtime difference. Very large mapped types may affect checker speed—rare in app code.

Does export type differ from export interface?

Both export names. With isolatedModules, export type may be required for type-only re-exports in some bundler setups (export type { User }).

eslint says “Use type only” — must I obey?

Follow team standards. The guidance is consistency, not a language limitation.

When should I use Record<string, T>?

Record is a built-in generic alias for string-keyed objects—great for dictionaries; see utility types chapter for details.