Master the real differences between type aliases and interfaces in TypeScript — when to use each, how declaration merging works, which extends which, and how to answer every interviewer question on this perennial topic with confidence.
Think of an interface as a named contract posted on a wall — any team member (object or class) can sign it, and you can add new clauses to it later (declaration merging). A type alias is more like a sticky note with a formula written on it — it can describe anything (unions, primitives, tuples, computed shapes), but once you stick it somewhere, you cannot add more to it. Both describe shapes; the difference is in flexibility, extensibility, and where each one shines.
Interfaces describe the shape of objects and classes and support declaration merging. Type aliases can describe any type — primitives, unions, tuples, mapped types, conditional types — and cannot be merged. For plain object shapes both work equally well; the differences matter at the edges.
// Interface — keyword-based, feels like a contract
interface User {
id: number;
name: string;
email?: string; // optional property
}
// Type alias — assignment-based, more like a named formula
type User = {
id: number;
name: string;
email?: string;
};
// Both produce identical structural types — TypeScript treats them the same
// when checking compatibility between objects and these definitions.
Type aliases can represent things interfaces simply cannot:
// Union types — impossible with interface
type Status = "active" | "inactive" | "pending";
type ID = string | number;
// Primitive aliases
type Milliseconds = number;
type UserName = string;
// Tuple types
type Coordinate = [number, number];
type NamedEntry = [string, ...number[]];
// Mapped types
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
// Function type shorthand
type Handler = (event: Event) => void;
// (interfaces can describe call signatures too, but the syntax is clunkier)
You can declare the same interface name multiple times and TypeScript merges all declarations into one shape. This is declaration merging and it is the feature that enables library augmentation:
// First declaration — in your library's types
interface Window {
myAnalytics: Analytics;
}
// Second declaration — in a consumer's augmentation file
interface Window {
myFeatureFlag: boolean;
}
// TypeScript merges them — Window now has BOTH properties
// This is how libraries like @types/jest add methods to global Jest types
// Same trick works for extending third-party types:
declare module "express-session" {
interface SessionData {
userId: string;
cart: CartItem[];
}
}
Declaration merging is the primary reason to chooseinterfaceovertypewhen writing library code or global augmentations. A type alias with the same name twice is a compile error.
Interfaces use extends. Type aliases use intersection (&). Both create a wider shape, but they behave differently when properties conflict:
// Interface extends — compile error on conflict
interface Animal {
name: string;
}
interface Dog extends Animal {
name: number; // Error: property 'name' in 'Dog' incompatible with 'Animal'
}
// Type intersection — silently produces 'never' on conflict
type Animal = { name: string };
type Dog = Animal & { name: number };
// Dog['name'] is string & number = never — no compile error here!
// This is a subtle and dangerous difference
// Interfaces extending type aliases — fully supported
type HasId = { id: number };
interface Product extends HasId {
title: string;
price: number;
}
// Type aliases intersecting interfaces — also fully supported
interface Named { name: string; }
type Employee = Named & { employeeId: string };
Classes can implement both interfaces and type aliases that describe object shapes — but not unions or primitives:
interface Serializable {
serialize(): string;
}
type Loggable = {
log(): void;
};
class UserService implements Serializable, Loggable {
serialize() { return JSON.stringify(this); }
log() { console.log(this.serialize()); }
}
// This does NOT work — cannot implement a union
type StringOrNumber = string | number;
class Broken implements StringOrNumber {} // Error
The TypeScript compiler can cache and reuse interface types more efficiently than complex type aliases in large projects. For simple object shapes the difference is negligible, but extremely complex intersection and conditional types can slow down type-checking in large monorepos. This is a secondary concern — choose the right abstraction first.
Many developers think interfaces and type aliases are completely interchangeable for object shapes — they are nearly identical for simple objects, but diverge on declaration merging (interfaces only), union types (type aliases only), and conflict handling in extends vs intersection (interfaces error; intersections silently produce never).
Many developers think 'type is more modern so prefer it always' — the TypeScript team itself recommends interfaces for object shapes in library code because declaration merging enables consumers to augment library types without forking them.
Many developers think you cannot extend a type alias with an interface — you can. interface Product extends HasId works even when HasId is a type alias. The extends keyword works across both constructs.
Many developers think type intersection (&) always errors on conflicting properties like interface extends does — it does not. Conflicting property types are silently narrowed to never, which produces type-safe but often baffling 'this value is never assignable' errors downstream.
Many developers think interfaces can describe union types — they cannot. type Result = Success | Failure is valid; interface Result = Success | Failure is a syntax error.
Many developers think declaration merging only applies to global types — it applies to any interface, including module-level ones. This is how session augmentation, express Request extensions, and Jest global matchers are typed.
Module augmentation in Express: teams extend the Request interface to add custom properties like req.user or req.session.cart. Because interfaces merge, you can add to the third-party definition without touching the library source.
API response shapes: most REST client code uses type aliases for discriminated unions like type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string } — this union pattern is impossible with an interface.
Design system component props: libraries like Radix UI and Chakra define component props as interfaces so consumers can use module augmentation or interface extension to add custom variants without forking the library.
Domain model types: id, price, and currency types are commonly aliased as type UserId = string or type Price = number to document intent and prevent mixing up unrelated string or number values across a codebase.
Global type extensions: @types/jest adds expect, describe, and it to the global scope by merging into NodeJS.Global — only possible because interfaces support declaration merging.
typeof narrowing determines which branch runs
What is type inference in TypeScript and when does it kick in?
What is the difference between any, unknown, and never in TypeScript?
What is the difference between type and interface in TypeScript?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.