type vs interface in TypeScript - What You Really Need to Know
Current Situation Analysis
Developers frequently encounter the type vs interface debate, often relying on oversimplified community heuristics like "use interface for objects, type for everything else." This superficial guidance ignores TypeScript's underlying type system mechanics, leading to inconsistent codebases and hidden failure modes.
Pain Points & Failure Modes:
- Silent Type Degradation: Using
&for object extension masks property conflicts, resolving toneverinstead of throwing immediate compile-time errors. - Augmentation Failures: Attempting to extend global objects or third-party library types with
typeaccidentally overwrites the original definition instead of merging, causing type collisions. - Unintentional Type Pollution: Using
interfacefor data transfer objects (DTOs) or API payloads enables accidental declaration merging across modules, breaking type isolation. - Why Traditional Methods Fail: Rule-of-thumb guidelines treat
typeandinterfaceas interchangeable syntactic sugar. In reality, they map to fundamentally different compiler behaviors: structural naming (type) vs. contract declaration with merge capabilities (interface). Without understanding declaration merging, error reporting phases, and type expression limitations, teams introduce brittle type definitions that fail at scale.
WOW Moment: Key Findings
| Approach | Declaration Merging | Union/Conditional Support | Conflict Error Reporting | Augmentation Safety | Recommended Scope |
|---|---|---|---|---|---|
interface | β Automatic merge | β Syntax error | β Immediate at declaration | β Safe for globals/libs | Library APIs, extensible contracts |
type | β Duplicate identifier error | β Full support | β οΈ Delayed (resolves to never) | β Overwrites original | DTOs, unions, mapped/conditional types |
Key Findings:
interfaceandtypeare functionally identical for object shapes, generics, and basic extension.- The critical divergence lies in declaration merging (
interfaceonly) and type expression flexibility (typeonly). - Error reporting behavior differs significantly:
extendsfails fast at definition, while&defers failure to usage, creating a silentnevertrap.
Core Solution
1. Structural Equivalence
Before addressing differences, it's critical to recognize where both constructs produce identical compiler output.
Describing object shapes
interface User {
id: number;
name: string;
}
type User = {
id: number;
name: string;
};
Identical result. Neither is "better" for describing objects.
Generics
interface Box<T> {
value: T;
}
type Box<T> = {
value: T;
};
Both support generics. No difference here.
Extension
// interface extends interface
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// type & type
type Animal = { name: string };
type Dog = Animal & { breed: string };
// interface extends type (yes, this works)
type Animal = { name: string };
interface Dog extends Animal {
breed: string;
}
// type & interface (this works too)
interface Animal {
name: string;
}
type Dog = Animal & { breed: string };
All four variants compile without issues. extends and & are practically equivalent in most cases, with one exception covered below.
2. Fundamental Divergence
Unions - type only
type Status = "active" | "inactive" | "pending"; // β
interface Status = "active" | "inactive" | "pending"; // β syntax error
interface cannot describe a union. Its syntax requires curly braces and a list of propert
ies. If you need a union, then you have to use type.
Same goes for conditional types, mapped types, and tuple types:
type MaybeString = string | null;
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pair = [string, number];
None of the above can be expressed with interface.
Declaration merging - interface only
This is the difference that has real practical consequences.
An interface with the same name declared in two places does not cause an error, it merges:
interface Config {
timeout: number;
}
interface Config {
retries: number;
}
// Result: Config = { timeout: number; retries: number }
const c: Config = {
timeout: 3000,
retries: 3,
};
With type, this is a compile error:
type Config = { timeout: number };
type Config = { retries: number }; // β Duplicate identifier 'Config'
Type conflict during merging What if both interfaces declare the same field with conflicting types?
interface A {
x: number;
}
interface A {
x: string; // β error - conflict during merging
}
The compiler reports the error at the declaration site, not at the point of use. The merge attempts to combine number and string on field x which is impossible.
extends vs & - difference in error reporting
Technically you get the same result, but the compiler behaves differently when something goes wrong.
interface A {
x: number;
}
interface B extends A {
x: string; // β immediate error: "Interface 'B' incorrectly extends interface 'A'"
}
type A = { x: number };
type B = A & { x: string };
// No error at declaration!
// But B.x has type: never
With &, the compiler won't complain at the definition, you'll get never on field x, which only surfaces when you try to use it. This is a subtle trap: never instead of an error can slip through code review unnoticed.
3. Declaration Merging in Practice
Augmenting global objects
You want to add a field to the built-in Window:
// globals.d.ts
declare global {
interface Window {
analytics: Analytics;
}
}
Why doesn't type work here?
type Window = { analytics: Analytics }; // β
This creates a new type named Window that conflicts with the built-in Window. You're trying to overwrite, instead of extending the original.
interface uses declaration merging. Your declaration gets merged into the existing one. The original Window still exists, analytics is added onto it.
Augmenting types from external libraries
A library exports an interface you want to extend without touching node_modules:
// types/tailwindcss-vite.d.ts
import "@tailwindcss/vite";
declare module "@tailwindcss/vite" {
interface Theme {
borderRadius: number;
}
}
The mechanism: declare module tells the compiler "this is an augmentation of an existing module." Inside, you use declaration merging on interface Theme adding a field to what the library already exports.
Note: you import the module (import "@tailwindcss/vite") before augmenting it. Without this, the compiler doesn't know what to look for.
Using type instead of interface here won't work. type is not subject to augmentation.
Pitfall Guide
- Silent
neverType Trap: Using&to merge conflicting properties suppresses immediate compiler errors. The conflict resolves tonever, which only surfaces when the property is accessed, making itζζ slip through code review and CI pipelines. - Declaration Merging Conflicts: Declaring the same
interfacename in multiple files with conflicting property types causes a compile-time error at the declaration site. This is safer than&but requires strict coordination in large codebases to avoid accidental collisions. - Accidental Type Overwriting: Using
typeinstead ofinterfacefor global or library augmentation creates a new, isolated type rather than merging. This breaks runtime expectations and causesProperty 'X' does not exist on type 'Y'errors. - Missing Module Import for Augmentation: When augmenting external libraries, failing to include
import "module-name"beforedeclare moduleprevents the compiler from resolving the target namespace. The augmentation silently fails or creates a new module scope. - Misapplying Declaration Merging: Using
interfacefor DTOs, API payloads, or internal data structures enables unintended cross-module merging. This leads to type pollution where unrelated modules accidentally extend each other's contracts. - Assuming Interchangeability: Treating
typeandinterfaceas identical ignores fundamental limitations.interfacecannot express unions, mapped types, or conditional types, whiletypecannot participate in declaration merging. Forcing one pattern everywhere creates technical debt.
Deliverables
- TypeScript Type Strategy Blueprint: A decision matrix mapping type definitions to architectural boundaries (e.g., DTOs β
type, extensible contracts βinterface, utility transformations βtype). Includes module augmentation patterns and merge-safe naming conventions. - Type vs Interface Selection Checklist: A 10-point validation checklist for code reviews covering union/intersection needs, declaration merging intent, error-reporting expectations, and augmentation requirements.
- Configuration Templates: Pre-configured
.d.tstemplates for safe global augmentation, library extension, and merge-controlled interface definitions. Includestsconfigflags to enforce strict declaration merging behavior and prevent accidental type overwrites.
