---
title: "TypeScript: Types vs Interfaces"
slug: "typescript-types-vs-interfaces"
category: "coding"
tags: ["coding", "typescript", "types", "interfaces", "api"]
status: "stable"
last_updated: 2026-05-14
summary: "Use interface for public object-shaped APIs that may need declaration merging; use type for everything else."
related: ["[[coding/typescript]]", "[[coding/typescript-generics]]", "[[coding/typescript-utility-types]]", "[[coding/typescript-narrowing]]", "[[frontend/react]]"]
---

## 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 [[coding/typescript]] for the umbrella rules and [[coding/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.

```ts
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.

```ts
// 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.

```ts
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.

```ts
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 [[coding/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`.

```ts
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 [[coding/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.

```tsx
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 [[frontend/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 [[coding/typescript-generics]] for the generic patterns that work with both.

## Related

- [[coding/typescript]]
- [[coding/typescript-generics]]
- [[coding/typescript-utility-types]]
- [[coding/typescript-narrowing]]
- [[frontend/react]]
