Blog
3 min read

type vs interface in TypeScript: Which Should You Use?

type aliases and interfaces both describe object shapes, and mostly behave the same. The real differences — unions and other non-object types, declaration merging, extends vs intersections, error messages — and a simple, consistent rule for choosing in your codebase.

TypeScript gives you two ways to name the shape of an object:

type User = {
  id: string
  email: string
}

interface User {
  id: string
  email: string
}

For most everyday code, they're interchangeable. The differences matter at the edges. (What is TypeScript?)

What only type can do

type can name any type, not just objects:

type Status = 'draft' | 'sent' | 'paid'                 // union of literals
type Id = string | number                               // union
type Point = [number, number]                           // tuple
type Handler = (event: Event) => void                   // function
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string }   // discriminated union
type ReadonlyUser = Readonly<User>                      // mapped/utility types
type Keys = keyof User                                  // computed

Unions — especially discriminated unions — are one of TypeScript's most useful features, and they need type. (TypeScript generics)

What only interface can do

Declaration merging

Two interfaces with the same name merge:

interface Window {
  analytics: Analytics
}

This adds analytics to the built-in Window type. That's how you extend globals and third-party library types. A type with a duplicate name is an error. (Property does not exist on type)

The flip side: merging can happen by accident if two files declare the same interface name in the same scope.

Extending

interface Admin extends User {
  permissions: string[]
}

type Admin = User & {
  permissions: string[]
}

Both work. Differences:

  • extends errors if the new property conflicts with an existing one; an intersection (&) silently produces an impossible type (never) for conflicting properties.
  • Interfaces with extends tend to give clearer error messages and can be slightly faster for the compiler in very large codebases.

Classes

Classes can implements either an interface or an object type alias. No practical difference.

Side by side

type interface
Object shapes ✅ ✅
Unions, tuples, functions, primitives ✅ ❌
Mapped and conditional types ✅ ❌
Declaration merging ❌ ✅
Extending & intersection extends (catches conflicts)
Augmenting library/global types ❌ ✅

A simple rule

Pick a convention and stick to it. Two common ones:

Option A — type by default. Use type everywhere, and interface only when you need declaration merging (globals, library augmentation). Works well in app code full of unions — React props, API results, state.

Option B — interface for objects, type for everything else. The approach the TypeScript handbook has historically leaned towards, common in libraries and larger codebases.

Either is fine. What hurts is mixing them randomly. A lint rule (@typescript-eslint/consistent-type-definitions) can enforce your choice. (Linters and formatters)

React props

Both are common:

type ButtonProps = { variant?: 'primary' | 'ghost'; children: React.ReactNode }
interface ButtonProps { variant?: 'primary' | 'ghost'; children: React.ReactNode }

With type you can easily combine with unions and utilities (ButtonProps & React.ComponentProps<'button'>), which is why many React codebases default to it. (React props explained)

The summary

  • Mostly interchangeable for object shapes.
  • Need unions, tuples or computed types → type.
  • Need to augment a global or library type → interface.
  • Choose a team convention and lint for it.

EasySpawn runs your TypeScript project with Claude Code alongside, which will follow whichever convention you put in your CLAUDE.md. See how it works or join the waitlist.

Related: TypeScript Generics Explained · JavaScript vs TypeScript · Type Is Not Assignable to Type · How to Write a CLAUDE.md That Actually Helps

Keep reading