Intersecting two object types with a conflicting property doesn't error — it silently produces `never` for that property, confirmed by compiling exactly that. This is one of several concrete, verifiable differences between type aliases and interfaces, replacing the usual 'just pick one, it doesn't matter' folklore with the actual mechanics.
1. Which of these can a `type` alias express directly that `interface` genuinely cannot?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
type ALIASES anything: type Id = string | number; ✅
type Cb = (x: number) => void; ✅
interface is RESTRICTED to object-like/callable shapes:
interface Id = string | number { } ❌ syntax error
DECLARATION MERGING — interface-ONLY:
interface Box { width: number }
interface Box { height: number } // ✅ MERGES
type Circle = { radius: number };
type Circle = { color: string }; // ❌ "Duplicate identifier"
CONFLICT RESOLUTION differs:
interface A extends B { } // conflicting prop → ERROR at extends itself
type Z = X & Y; // conflicting prop → compiles, but that
// property's type becomes `never`
// (error surfaces LATER, when something
// tries to actually USE/construct it)
WHEN EACH WINS:
type → unions, primitives, tuples, function signatures
interface → object shapes meant to be publicly EXTENSIBLE
(declaration merging is a real, usable capability there)
Plain, non-extensible object shape → mostly doesn't matter which.
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 directlytype Point = { x: number; y: number }; // an object shape — same as interface CAN dotype Callback = (x: number) => void; // a FUNCTION type, clean syntaxconst 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.
type Id = string | number; // ✅ compiles — type alias directly names a unioninterface Id = string | number { } // ❌ syntax error — interfaces can't alias unions/primitives at all
Confirmed: 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.
interface Box { width: number }interface Box { height: number } // MERGES — Box now requires bothtype 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 incompatiblytype X = { prop: string };type Y = { prop: number };type Z = X & Y;
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.
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.
Build a small demonstration of when each form is the more natural fit:
// type alias: naturally fits a union of DIFFERENT shapestype ApiResult = | { status: "success"; data: string[] } | { status: "error"; message: string };function handle(result: ApiResult) { if (result.status === "success") { console.
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.
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.
1. Treating type vs. interface as a purely stylistic, inconsequential choice#
// "it doesn't matter which I pick" — mostly true, but NOT always
For 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 conflict
The 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 merge
This specific pattern only works with interface — attempting it with type is a compile error, not a quieter version of the same merging behavior.
Reach for type when aliasing a union, a primitive, a tuple, or a function signature — these are things interface genuinely cannot express directly.
Reach for interface for 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.
Both type aliases and interface declarations 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, make tsc'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.
// compiles FINE — but Z["prop"] resolves to `never`
const z: Z = { prop: "hello" }; // ❌ Error: Type 'string' is not assignable to type 'never'
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 {
name: string;
}
// a consumer, in THEIR OWN code, can add to it via declaration merging:
interface PluginConfig {
version: string; // extending a library's exposed interface, without editing the library