Type Aliases vs Interfaces in TypeScript: The Real Difference
This is the single most-asked TypeScript question after โwhatโs any vs unknown,โ and most answers youโll find are outdated, overly dogmatic, or just wrong about what actually differs. The honest answer: for describing plain object shapes, type and interface are close enough to interchangeable that team convention matters more than technical necessity. But they are not identical, and the differences that do exist have real consequences once your codebase grows or starts consuming third-party libraries.
Where They Look Identical
interface UserInterface { name: string; age: number;}
type UserType = { name: string; age: number;};
function greetInterface(user: UserInterface) { return `Hi ${user.name}`; }function greetType(user: UserType) { return `Hi ${user.name}`; }
greetInterface({ name: "Alice", age: 30 }); // worksgreetType({ name: "Alice", age: 30 }); // works โ identical shape, identical behaviorFor this exact case โ a plain object shape โ the compiled output is identical (both erase completely), the structural typing rules apply identically, and either one type-checks the same calling code the same way. If your team has no strong convention, this case genuinely doesnโt matter. The differences show up at the edges.
Difference 1: type Can Alias Anything; interface Only Describes Object Shapes
type ID = string | number; // union โ interface cannot do thistype Coordinates = [number, number]; // tuple โ interface cannot do thistype Handler = (event: Event) => void; // function type โ interface CAN do this, awkwardlytype Nullable<T> = T | null; // generic alias over any type โ interface cannot do this
// This is NOT possible with interface:// interface ID = string | number; โ syntax error, interfaces can't alias a uniontype is a general-purpose naming mechanism โ it can alias a union, an intersection, a tuple, a primitive, a mapped type, or an object shape. interface is specifically for describing the shape of an object (or a callable, as covered in the interfaces guide) โ it has no syntax for aliasing a union or a primitive at all. This alone settles a large share of the โwhich oneโ question: if what youโre naming isnโt a plain object shape, type is your only option.
Difference 2: Declaration Merging
interface Config { apiUrl: string;}
interface Config { timeout: number;}
// Merged automatically โ Config now requires BOTH apiUrl and timeoutconst config: Config = { apiUrl: "https://api.example.com", timeout: 5000 };type ConfigType = { apiUrl: string;};
type ConfigType = { timeout: number;};// Error: Duplicate identifier 'ConfigType'Declaring an interface twice with the same name merges the two declarations into one combined shape (covered in depth in the interfaces guide); declaring a type alias twice with the same name is a hard compile error. This is the sharpest, least negotiable difference between the two โ and itโs exactly why library type definitions (like Expressโs Request, or the global Window object) are almost always written as interface, not type: itโs the only one of the two that lets consumers extend the shape from their own code without modifying the original declaration.
Difference 3: Intersections vs Extends
// interface: extendsinterface Animal { name: string;}interface Dog extends Animal { breed: string;}
// type: intersection (&)type AnimalType = { name: string;};type DogType = AnimalType & { breed: string;};These look nearly equivalent, and for straightforward cases they behave the same. The difference emerges with conflicting property types: interface extends errors immediately if the extending interface redeclares an inherited property with an incompatible type, while a type intersection silently collapses conflicting types down to never for that property, deferring the error until you actually try to use it.
interface A { prop: string; }interface B extends A { prop: number; }// Error immediately: Interface 'B' incorrectly extends interface 'A' โ types of property 'prop' are incompatible
type AT = { prop: string };type BT = AT & { prop: number };// No error here! BT.prop's type silently becomes `never`// (string & number has no valid values โ the error only surfaces later, when you try to assign to prop)This is a real, cited reason some style guides prefer interface for object shapes specifically: a conflicting property type is caught immediately at the point of declaration, rather than silently producing an unusable never type that only fails later, at a point further from the actual mistake.
Difference 4: Readability for Complex Unions
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "rectangle": return shape.width * shape.height; }}Discriminated unions like this โ one of the most useful patterns in the entire type system โ are only expressible as type. Thereโs no interface syntax for โone of these three distinct shapes.โ Any time the shape youโre modeling is genuinely โthis OR that,โ not โalways these properties,โ type is the only tool that fits.
Performance: Does It Actually Matter?
TypeScriptโs own team has noted that, for large, deeply-nested structural type checks, interface can type-check marginally faster than an equivalent type intersection, because interfaces are cached and compared by declaration identity in some cases, while intersections must be recomputed more often. In practice, this difference is measurable only in very large codebases with pathologically complex type compositions โ it should never be the deciding factor for an application of normal size, and no team should choose interface over type purely for this reason without profiling an actual compile-time problem first.
A Practical Rule of Thumb
| Situation | Use |
|---|---|
| Describing the shape of an object or a classโs public API | interface โ clearer stack traces, supports extends, and if itโs a public library type, supports consumer augmentation |
| Union types, discriminated unions | type โ interface cannot express this at all |
| Tuple types, function types, mapped/conditional types | type โ again, not expressible as interface |
| A shape you explicitly want to prevent from being re-declared/merged elsewhere | type โ the compile error on duplication is a feature here, not a limitation |
| Youโre writing type declarations for a library others will extend | interface โ declaration merging is the mechanism consumers rely on |
Some teams enforce this consistently with an ESLint rule:
{ "rules": { "@typescript-eslint/consistent-type-definitions": ["error", "interface"] }}This flags any plain object shape declared with type instead of interface (unions, intersections, and other non-object-shape types are unaffected by the rule), which removes the โwhich one did we use last timeโ bikeshedding from code review entirely โ the linter enforces the convention instead of a human needing to remember it.
Many teams adopt a simple default โ โuse interface for object shapes, type for everything else (unions, tuples, function types, generics)โ โ and that single rule resolves the overwhelming majority of real cases without needing to memorize every edge-case difference above.
Generics Work on Both
interface Box<T> { value: T;}
type BoxType<T> = { value: T;};
const stringBox: Box<string> = { value: "hello" };const numberBox: BoxType<number> = { value: 42 };Both support generic type parameters with identical syntax and identical behavior for plain object shapes โ this isnโt a differentiator between them. Where it does diverge is in more advanced generic constructs: conditional types, mapped types, and infer (covered in the mapped and conditional types guide) can only be expressed with type โ thereโs no interface syntax for a conditional type at all.
Recursive Types
// Both interface and type support self-reference for object shapesinterface TreeNode { value: number; children: TreeNode[];}
type JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };Interfaces have always supported recursive self-reference naturally, since an interfaceโs properties can reference the interfaceโs own name directly. type aliases gained robust support for recursive references (like the JSONValue example, which models literally any valid JSON value) more recently in the languageโs history โ for a genuinely recursive union like this, type is the only option anyway, since unions arenโt expressible as interface at all.
What Neither One Does
Neither type nor interface produces anything at runtime โ both are compile-time-only constructs, fully erased before your code runs (see how TypeScript works for why). This means neither can be used with instanceof, neither shows up if you console.log an objectโs constructor, and neither adds a single byte to your compiled JavaScript output regardless of how large or deeply nested the type definition is.
interface User { name: string; }type UserT = { name: string; };
const obj = { name: "Alice" };console.log(obj instanceof User); // compile error โ User has no runtime existence to check againstFrequently Asked Questions
If Iโm not sure, which should I default to? interface for anything describing an objectโs shape, type for anything else. This single heuristic handles the vast majority of real decisions without requiring the deeper edge cases in this guide.
Can I convert an interface to a type (or back) without breaking anything? For plain object shapes with no re-declaration/merging happening elsewhere in the codebase, usually yes โ but search first for any other declaration of the same interface name (declaration merging is easy to miss when itโs spread across multiple files, which is common in larger codebases and library type definitions).
Does using type instead of interface change my compiled JavaScript output? No โ never. Both are completely erased at compile time regardless of which one you choose; the entire difference lives in what the compiler checks before your code runs, not in what runs.
Why do library .d.ts files almost always use interface for exported object types? Because declaration merging lets consumers of the library extend those types from their own code (adding a custom property to Request, for instance) without needing the library itself to anticipate every possible extension. A type alias canโt be augmented this way at all โ thatโs a real, load-bearing reason, not just convention.
Is there a case where mixing type and interface for the same conceptual shape causes real problems? Yes โ a type alias for a union or intersection generally cannot be used with extends-style narrowing the way an interface hierarchy can, and an interface extending a type alias that resolves to a union will produce confusing errors. Keep one shapeโs entire declaration in one form rather than mixing type and interface for the same evolving concept.
Will this decision ever need revisiting as a codebase grows? Rarely for the object-shape case, since both erase identically and neither imposes a runtime or bundle-size cost. The one scenario worth watching for: a shape that started as a simple object and later needs to become a discriminated union (adding a kind field with variants) โ at that point, an interface has to be converted to type anyway, since unions arenโt expressible as interface. Anticipating that possibility is a reasonable, if minor, argument for defaulting new object shapes to type in codebases that expect a lot of evolving domain modeling.
Whatโs Next
You now have a concrete, edge-case-aware answer to a question most resources leave vague. Next: Union and Intersection Types, where we go deeper into the | and & operators that type aliases unlock โ including the discriminated union pattern briefly shown above, which is one of the most practically useful tools in the entire type system.