Overview

interface and type overlap on the 80% case and diverge on the edges. The diverging cases are what the choice rides on: declaration merging, intersection vs extension, and how the compiler displays errors. Read typescript for the umbrella rules and typescript-utility-types for the helpers that work with both.

Default to type for everything

Pick type as the default. It covers unions, intersections, mapped types, conditional types, tuples, and primitives. interface cannot express any of those.

type Status = "draft" | "sent" | "paid"
type UserId = string & { readonly brand: unique symbol }
type Entries<T> = { [K in keyof T]: [K, T[K]] }[keyof T]

The “always use interface” rule from older guides is a holdover from a TypeScript version that no longer exists. A modern codebase reaches for type first and switches to interface only when a specific feature calls for it.

Use interface for public object-shaped APIs

Reach for interface when both are true: the shape is an object, and the shape is part of a public API that consumers might augment. The compiler’s error messages name interfaces by their declared name; type aliases sometimes display as the resolved structural form.

// Library code: consumers might want to add to the request shape.
export interface RequestContext {
  requestId: string
  userId: string | null
}
 
// Application code can augment the library type.
declare module "@acme/http" {
  interface RequestContext {
    traceId: string
  }
}

This is the only case where interface wins clearly. Most application code never needs declaration merging; in that code, type is fine.

Declaration merging belongs to libraries

interface declarations with the same name in the same scope merge. type aliases do not; redeclaring a type is a compile error.

interface Window { acme?: Acme }
interface Window { acme?: Acme }  // merges; both fields now optional Acme
 
// type Window = ...  // duplicate identifier error

This is load-bearing for ambient libraries (Express’s Request, Node’s global types, Vite’s import.meta.env). It is almost never useful in your own application code; treat accidental merging as a bug.

Extend with extends; combine with intersections

interface B extends A and type B = A & { ... } look interchangeable. They are not.

  • extends checks for conflicts at the declaration site. A subclass field that conflicts with the parent is a compile error.
  • & resolves conflicts by intersecting the field types, which can produce never silently when fields disagree.
interface A { x: string }
interface B extends A { x: number }  // Error: incompatible override
 
type A2 = { x: string }
type B2 = A2 & { x: number }
// B2.x is string & number = never. Compiles. Bug.

Use extends when the relationship is “B is an A plus more.” Use & when you genuinely want to combine independent shapes.

Perf: prefer interface for deep extension chains

The TypeScript compiler caches interface extensions more aggressively than type intersections. A deep chain of type A = B & { ... } re-evaluates on every use; the same chain with interface A extends B is cached.

For most app code the difference is invisible. For library code with hundreds of derived shapes, the difference shows up in tsc runtime. If a build is slow and the type graph is deep, switching to interface extends is one of the first knobs to turn. See typescript-tsconfig for the other compile-time levers.

Unions, primitives, and mapped types are type-only

interface cannot express any of the following. If you need any of them, the answer is type.

type Result<T> = { ok: true; value: T } | { ok: false; error: Error }
type ID = string                              // alias for a primitive
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type FirstArg<F> = F extends (a: infer A, ...rest: never) => unknown ? A : never

The discriminated union (Result<T> above) is the single most useful pattern in TypeScript. See typescript-narrowing for how the compiler narrows on the discriminant.

React: prefer type for props

React component props are not extended by consumers in normal code. Use type; the union, intersection, and utility-type ergonomics pay off.

type ButtonProps = {
  variant: "primary" | "secondary"
  size?: "sm" | "md" | "lg"
  children: React.ReactNode
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children">
 
export function Button({ variant, size = "md", children, ...rest }: ButtonProps) {
  return <button data-variant={variant} data-size={size} {...rest}>{children}</button>
}

Use interface for props only if you are publishing a component library where consumers might augment the prop shape via module augmentation. See react for the broader component rules.

The one-line rule

interface for public, object-shaped APIs that may need declaration merging. type for everything else. When in doubt, type. See typescript-generics for the generic patterns that work with both.