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:
extendserrors if the new property conflicts with an existing one; an intersection (&) silently produces an impossible type (never) for conflicting properties.- Interfaces with
extendstend 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
Webpack vs Vite: What a Bundler Does and Which One to Use
Bundlers turn your source files into the optimised JavaScript and CSS a browser loads. How Webpack and Vite differ — bundling everything up front vs serving native modules in development — speed, configuration, migration, and where Turbopack and Rspack fit.
Vitest vs Jest: Which JavaScript Test Runner Should You Use?
Jest was the default JavaScript test runner for years; Vitest is the fast, Vite-native alternative with a Jest-compatible API. How they compare on speed, ESM and TypeScript support, config, mocking, browser mode and ecosystem, plus migrating from Jest to Vitest.