Interfaces vs Type Aliases
📖 Concept
TypeScript provides two main ways to define object shapes: interfaces and type aliases. They overlap significantly but have key differences.
Interface — Defines a contract for object shapes. Can be extended and merged (declaration merging). Type Alias — Creates a name for ANY type — objects, unions, intersections, primitives, tuples. Cannot be merged.
When to use which (practical rule):
- Use interface for object shapes, class contracts, and public APIs (extendable, mergeable)
- Use type for unions, intersections, mapped types, conditional types, and utility types
- In practice, many teams pick one and use it consistently — both work fine for objects
Declaration merging is unique to interfaces: if you declare the same interface name twice, TypeScript merges them. This is how libraries extend global types (e.g., adding properties to Window).
extends vs & (intersection):
interface B extends A— creates a subtype with explicit relationshiptype B = A & { extra: string }— creates an intersection type
Both achieve similar results, but extends gives better error messages and is checked more eagerly.
🏠 Real-world analogy: An interface is like a job description — it lists required skills, and anyone who matches them qualifies. A type alias is like a label — you can label anything: a single item, a combination, or even a condition.
💻 Code Example
1// Interface — object shape contract2interface User {3 id: number;4 name: string;5 email: string;6 age?: number; // Optional property7 readonly createdAt: Date; // Cannot be modified after creation8}910// Extending interfaces11interface Admin extends User {12 role: "admin" | "superadmin";13 permissions: string[];14}1516// Type alias — can describe ANY type17type ID = string | number; // Union18type Pair<T> = [T, T]; // Generic tuple19type Callback = (data: string) => void; // Function type20type Status = "loading" | "success" | "error"; // String literal union2122// Type alias for objects (works like interface)23type Product = {24 id: number;25 name: string;26 price: number;27};2829// Intersection types (like extending)30type AdminUser = User & {31 role: string;32 permissions: string[];33};3435// Declaration merging — ONLY works with interfaces36interface Window {37 myCustomProperty: string; // Merges with the global Window interface38}3940// interface + declaration merging example41interface Config {42 apiUrl: string;43}44interface Config {45 timeout: number; // Merged! Config now has BOTH apiUrl and timeout46}47const config: Config = {48 apiUrl: "https://api.example.com",49 timeout: 500050};5152// Readonly and optional modifiers53interface Article {54 readonly id: number;55 title: string;56 content: string;57 tags?: string[]; // Optional58 readonly author: string;59}6061const article: Article = {62 id: 1,63 title: "TypeScript Guide",64 content: "...",65 author: "Alice"66};67// article.id = 2; // ❌ Error: Cannot assign to 'id' because it is read-only6869// Index signatures — dynamic keys70interface StringMap {71 [key: string]: string;72}73const headers: StringMap = {74 "Content-Type": "application/json",75 "Authorization": "Bearer token123"76};
🏋️ Practice Exercise
Mini Exercise:
- Create an
interface Userand atype User— observe that both work for object shapes - Extend an interface with
extendsand create an equivalent using&intersection - Use declaration merging to add a custom property to the global
Windowinterface - Create a type alias for a union of string literals (e.g., HTTP methods)
- Create an interface with
readonlyand optional (?) properties — try modifying them
⚠️ Common Mistakes
Thinking interfaces and type aliases are completely interchangeable — type aliases can represent unions, tuples, and primitives; interfaces cannot
Not knowing about declaration merging — interfaces with the same name in the same scope are automatically merged, which can cause surprising behavior
Using
&(intersection) whenextendswould give clearer error messages — intersection conflicts produce confusing typesConfusing
readonlywith deep immutability —readonlyonly prevents reassignment of the property itself, not mutation of nested objectsOver-using index signatures
[key: string]: any— this weakens type safety; useRecord<string, SpecificType>or a proper interface instead
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Interfaces vs Type Aliases