TypeScript

Interfaces vs type aliases

Two different syntaxes for describing the same object shape, overlapping enough to cause genuine confusion about which to reach for — plus the handful of things only one of them can actually do.

Beginner

4 min read

Both describe the same shape, and TypeScript treats them almost identically

interface UserI {
  id: number;
  name: string;
}
 
type UserT = {
  id: number;
  name: string;
};
 
const a: UserI = { id: 1, name: "Ada" };
const b: UserT = { id: 1, name: "Ada" };
 
const c: UserI = b; // fine — structurally identical, interface vs type doesn't matter here

UserI and UserT describe the exact same object shape, and thanks to structural typing (from the previous lesson), a value satisfying one satisfies the other — TypeScript doesn't care which syntax declared a type when checking compatibility. For describing a plain object shape, interface and type are functionally interchangeable, which is exactly why the choice between them so often comes down to team convention rather than a hard technical requirement.

Where they genuinely diverge: declaration merging

interface Window {
  myGlobalFlag: boolean;
}
// Elsewhere, even in a different file:
interface Window {
  anotherFlag: string;
}
// Window now has BOTH myGlobalFlag and anotherFlag — merged automatically
 
type Config = { debug: boolean };
type Config = { verbose: boolean }; // Error: Duplicate identifier 'Config'

Declaring the same interface name twice merges the two declarations into one combined interface — this is a real, intentional TypeScript feature called declaration merging, not a bug. A type alias with the same name declared twice is a hard compile error instead. Merging is exactly how you extend a built-in or third-party type you don't control the source of — augmenting the global Window interface with a custom property is the single most common real use, letting window.myGlobalFlag type-check correctly project-wide without editing a library's own .d.ts file.

Where the other diverges: type aliases can name things interfaces can't

type ID = string | number;                        // a union — interface has no equivalent
type Point = [number, number];                     // a tuple — interface has no equivalent
type Callback = (data: string) => void;             // a function type — interface can, but this reads cleaner
type Handler<T> = (event: T) => void;                // a generic alias over any T

interface is fundamentally an object-shape (or function-signature) description — it has no way to directly name a union type, a tuple type, or a primitive alias the way type can. Any time the thing being described isn't cleanly "an object with these fields," type is the only option, not a stylistic preference — this is the actual deciding factor in most real codebases, not extends vs &.

Extending: extends vs intersection (&), and why they usually behave the same

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}
 
type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

interface extension (extends) and type-alias intersection (&) both combine two shapes into one, and for straightforward object shapes they produce the same practical result — a value needs name and breed either way. The real difference shows up only in edge cases: an interface extends a conflicting property type is a compile error immediately at the extends clause, catching the conflict early and with a clear message; an & intersection with conflicting property types instead silently produces never for that property (an unsatisfiable type), which surfaces later and less clearly, wherever that property actually gets used.

The practical, low-stakes rule of thumb

// Public API surface, might need to be extended/merged by consumers later -> interface
interface ApiResponse {
  status: number;
  data: unknown;
}
 
// Everything else — unions, tuples, function types, mapped/conditional types -> type
type RequestMethod = "GET" | "POST" | "PUT" | "DELETE";

Neither the TypeScript team's own style guide nor the wider ecosystem enforces one hard rule, and plenty of production codebases pick one and use it almost everywhere out of consistency alone. A reasonable, common default: reach for interface when describing an object shape that's part of a public API and might benefit from declaration merging later, and reach for type for everything else — unions, tuples, function signatures, and any type built from other types rather than declared as a fresh object shape. This isn't a rule TypeScript enforces; it's closer to a convention worth picking deliberately and then applying consistently, rather than debating on every single declaration.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What happens if you declare `interface Window { a: boolean }` and then, elsewhere, declare `interface Window { b: string }` again?

2. Which of these can a `type` alias express that a plain `interface` cannot?

3. What's the practical difference between combining shapes with `interface extends` versus `&` (intersection) on type aliases, when the fields have conflicting types?