TypeScript's interface vs. type: What's Actually Different?
Break down the real differences between interface and type — declaration merging, extension syntax, and union types — and build a decision framework so you're not just guessing every time.
Two Things That Look Interchangeable
Write enough TypeScript and you'll eventually run into the same question: when you're defining the shape of an object, should you use interface or type?
interface User {
name: string
age: number
}
type User = {
name: string
age: number
}These two snippets behave identically in most situations — your editor's type hints and error checking barely differ. And because they look so similar on the surface, advice online tends to contradict itself: some say "use interface for objects, type for everything else," others say "just use type everywhere for consistency."
But keep writing TypeScript long enough and you'll eventually hit situations only one of the two can handle. Once you understand these key differences, choosing between them stops being about memorizing rules and becomes something you can derive directly from what you actually need.
Difference #1: Can It Merge?
interface has a capability type doesn't have at all: declaration merging. You can declare an interface with the same name more than once, and TypeScript automatically merges their properties into a single type.
interface Window {
title: string
}
interface Window {
width: number
}
// after merging, this is equivalent to
// interface Window {
// title: string
// width: number
// }
const w: Window = { title: 'App', width: 800 }This might feel counterintuitive at first, but it's a deliberate design choice. Its most common use is extending the types of a third-party package. Say you install a package that declares interface Config, but it's missing a field you need. You can declare that same interface name again in your own project and tack the field on — no need to touch the package's source code.
type can't do this. Declare a type with the same name twice and TypeScript throws an error right away:
type Window = { title: string }
type Window = { width: number } // error: duplicate declarationThis is the most fundamental difference between the two: interface describes a shape that can keep growing, while type describes an alias that's defined once and can never be changed.
Difference #2: The Extension Syntax Differs
Both can express "this type builds on another type, plus a few extra things" — but the syntax and semantics differ in subtle ways.
interface uses extends:
interface Animal {
name: string
}
interface Dog extends Animal {
breed: string
}type combines types through the & intersection operator instead:
type Animal = {
name: string
}
type Dog = Animal & {
breed: string
}Functionally, the two are close, but there's a real difference worth knowing: when you use extends and a new property conflicts with the parent type (say, the same name but an incompatible type), TypeScript throws an error right there on the extends line, and the message clearly names the conflicting interface. Combine conflicting properties with &, though, and TypeScript doesn't error at all — it just infers that property's type as never. The error doesn't surface until you actually use that property, which makes debugging more annoying.
interface A {
id: string
}
interface B extends A {
id: number // error: incompatible with property id in A
}
type C = { id: string }
type D = C & { id: number }
// D is defined successfully, but the type of id is inferred as neverFor object types with a clear hierarchy, interface extends tends to surface errors sooner and more directly.
Difference #3: type Can Express Far More Than interface
This is the difference that matters most in practice. interface can basically only describe the shape of objects (including functions and classes), while type can express a much wider range of types:
Union types, which interface simply can't express at all:
type Status = 'loading' | 'success' | 'error'
type ID = string | numberAliases for other kinds of types:
type Callback = (data: string) => void
type Tuple = [string, number]Conditional types and mapped types — scenarios where you're computing a type from another type — can only be written with type:
type NonNullableCustom<T> = T extends null | undefined ? never : T
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}In other words, interface covers just one slice of what type can express — the object-shape case specifically. The moment your needs involve union types, tuples, or computing something from an existing type, it stops being a matter of preference: type is your only option.
Does It Matter for Performance?
The TypeScript team and its docs have mentioned this in a few places: for large, structurally complex object types, interface benefits from an internal type-caching mechanism in the compiler, so type-checking tends to perform a bit better than the equivalent intersection type. In a small project you won't notice the difference at all, but in a large codebase where thousands of types compose together, it adds up into a compile-time difference you can actually measure. That's also why the official TypeScript style guide recommends: whenever interface can express an object shape, prefer it.
So How Do You Actually Choose?
Here's all of that boiled down into a few decision points:
- Describing a union type, a tuple, a function type alias, or something that needs conditional/mapped types?
typeis your only option — this isn't a matter of preference. - Need to let consumers extend a type you define (writing a package, or augmenting a global type like
Window)? Useinterfaceand lean on declaration merging. - Just describing the shape of an object or class, with no need to union it with other types? Either works, but favor
interface— the error messages are more direct and it has a slight performance edge. - Does your team already have a convention (say, the
consistent-type-definitionsESLint rule enforcing one or the other)? Follow it — consistency matters more than which one is technically "better."
In practice, a lot of teams land on a simple compromise: always use interface for object shapes, and type for everything else — union types, utility types, function signature aliases. That way you're not re-deciding every time, and it happens to line up exactly with what each one was designed to solve in the first place.
Wrapping Up
interface and type can both define an object's shape on the surface, but they were designed for different purposes. interface exists for extensible, mergeable object contracts — declaration merging supports incremental extension, and its errors are more direct. type is a more general-purpose type-aliasing tool that reaches into territory interface can't touch — union types, tuples, conditional types — at the cost of never being able to redeclare it once it's defined. Once you're clear on that difference in intent, the rest is just applying the right rule to the situation in front of you, instead of guessing.