Concept
The beginner framing: type aliases give a name to any type, not just object shapes like interface, but unions, primitives, tuples, and function signatures too.
type Id = string | number; // a UNION, interfaces can't do this directly
type Point = { x: number; y: number }; // an object shape, same as interface CAN do
type Callback = (x: number) => void; // a FUNCTION type, clean syntax
const id: Id = "abc-123";
const point: Point = { x: 1, y: 2 };
const cb: Callback = (x) => console.log(x);A lot of guidance on choosing between type and interface amounts to "just pick one, it doesn't really matter", which glosses over several real, verifiable capability differences worth actually knowing, not folklore.
Real difference #1: what each can alias
type Id = string | number; // ✅ compiles, type alias directly names a union
interface Id = string | number { } // ❌ syntax error, interfaces can't alias unions/primitives at allConfirmed: attempting to write an interface that directly aliases a union like this is a straight-up syntax error, interface is fundamentally restricted to describing object-like and callable shapes, while type can name literally any type expression, including unions, primitives, tuples, and function signatures.
Real difference #2: declaration merging
interface Box { width: number }
interface Box { height: number } // MERGES, Box now requires both
type Circle = { radius: number };
type Circle = { color: string }; // ❌ error: "Duplicate identifier 'Circle'"Confirmed by compilation: two interface declarations with the same name merge automatically (covered in Interfaces); the identical pattern with type produces a hard "Duplicate identifier" error instead. This isn't a minor detail, it's a genuine capability only interface has.
Real difference #3: how conflicts resolve when composing types
interface A { prop: string }
interface B { prop: number }
// interface A extends B {} // ❌ would ERROR, B's prop conflicts with A's incompatibly
type X = { prop: string };
type Y = { prop: number };
type Z = X & Y; // compiles FINE, but Z["prop"] resolves to `never`
const z: Z = { prop: "hello" }; // ❌ Error: Type 'string' is not assignable to type 'never'Confirmed by compilation: interface extension rejects an incompatible property conflict outright as a compile error at the extends declaration itself. Intersecting two conflicting object types with &, by contrast, compiles without complaint at the point of intersection, but the resulting property's type silently becomes never (since nothing can simultaneously be both a string and a number), which then makes the type practically impossible to construct, surfacing as an error only later, at the point something tries to actually provide a value for that property. Same underlying conflict, meaningfully different error-surfacing behavior.
Try It
Predict the outcome before checking the solution.
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const p: Person = { name: "Ada", age: 30 };
console.log(p);Does this compile? What about if HasAge were instead { name: number } (a genuine conflict with HasName's name: string)?
Solution
The original code compiles fine, no conflict, Person simply requires both name: string and age: number, and the object provides both correctly. If HasAge were changed to { name: number }, Person's name property would resolve to never (since it can't simultaneously be string AND number), and the object literal { name: "Ada", age: 30 } would then fail to compile with a "Type 'string' is not assignable to type 'never'" error on the name property specifically, the intersection itself still compiles, but nothing can actually satisfy it anymore.
Implement It Yourself
Build a small demonstration of when each form is the more natural fit:
// type alias: naturally fits a union of DIFFERENT shapes
type ApiResult =
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function handle(result: ApiResult) {
if (result.status === "success") {
console.log(result.data);
} else {
console.log(result.message);
}
}
// interface: naturally fits a single, extensible object shape,
// especially one a CONSUMER of your library might want to extend
interface PluginConfig {
This captures the practical shape of the decision: a union of genuinely different variants reaches naturally for type; an object shape meant to be extensible by other code (a common pattern in published library type definitions) benefits from interface's merging capability specifically.
Under the Hood
The intersection-conflict-produces-never behavior connects directly to Types's explanation of never as "a value that can genuinely never occur", a conflicting intersected property is a concrete, naturally-arising example of exactly that, not a special case invented just for this topic. And declaration merging's interface-exclusivity was already flagged as a forward reference in Interfaces, this is where that promise gets its full, compiler-verified treatment.
Common Mistakes
1. Treating type vs. interface as a purely stylistic, inconsequential choice
// "it doesn't matter which I pick", mostly true, but NOT alwaysFor a plain object shape, the choice often genuinely doesn't matter much, but the moment declaration merging, unions, or composing via intersection with a possible conflict enter the picture, the choice has real, compiler-verifiable consequences.
2. Expecting an intersection conflict to produce a clear error at the point of intersection
type Z = X & Y; // ❌ misconception: expecting an error HERE if X and Y conflictThe error (if any) doesn't surface at the intersection declaration itself, it surfaces later, wherever code tries to actually construct or use a value of the now-never-containing type. This delayed surfacing can make an intersection conflict genuinely confusing to trace back to its source.
3. Trying to declaration-merge a type alias like an interface
type Settings = { theme: string };
type Settings = { fontSize: number }; // ❌ "Duplicate identifier", NOT a mergeThis specific pattern only works with interface, attempting it with type is a compile error, not a quieter version of the same merging behavior.
Best Practices
- Reach for
typewhen aliasing a union, a primitive, a tuple, or a function signature, these are thingsinterfacegenuinely cannot express directly. - Reach for
interfacefor object shapes that are part of a public API surface where declaration merging (letting consumers extend the shape) is a genuinely useful capability. - Be deliberate about intersecting types that might conflict, since the resulting error (if any) surfaces later and less obviously than an equivalent interface-extension conflict would.
- Don't over-index on "it doesn't matter", for plain, non-extensible object shapes it mostly doesn't, but know the specific cases above where it genuinely does.
Performance Tips
- Both
typealiases andinterfacedeclarations are entirely compile-time constructs, neither has any runtime footprint or performance difference once compiled, consistent with TypeScript's general type-erasure model. - A deeply nested chain of intersections (
A & B & C & D & ...) can, in practice, maketsc's type-checking noticeably slower on very large types, a real, if secondary, engineering consideration when composing many types together, though this only affects build time, never runtime.
