Interfaces vs Type Aliases

0/6 in this phase0/21 across the roadmap

📖 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 relationship
  • type 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

codeTap to expand ⛶
1// Interface — object shape contract
2interface User {
3 id: number;
4 name: string;
5 email: string;
6 age?: number; // Optional property
7 readonly createdAt: Date; // Cannot be modified after creation
8}
9
10// Extending interfaces
11interface Admin extends User {
12 role: "admin" | "superadmin";
13 permissions: string[];
14}
15
16// Type alias — can describe ANY type
17type ID = string | number; // Union
18type Pair<T> = [T, T]; // Generic tuple
19type Callback = (data: string) => void; // Function type
20type Status = "loading" | "success" | "error"; // String literal union
21
22// Type alias for objects (works like interface)
23type Product = {
24 id: number;
25 name: string;
26 price: number;
27};
28
29// Intersection types (like extending)
30type AdminUser = User & {
31 role: string;
32 permissions: string[];
33};
34
35// Declaration merging — ONLY works with interfaces
36interface Window {
37 myCustomProperty: string; // Merges with the global Window interface
38}
39
40// interface + declaration merging example
41interface Config {
42 apiUrl: string;
43}
44interface Config {
45 timeout: number; // Merged! Config now has BOTH apiUrl and timeout
46}
47const config: Config = {
48 apiUrl: "https://api.example.com",
49 timeout: 5000
50};
51
52// Readonly and optional modifiers
53interface Article {
54 readonly id: number;
55 title: string;
56 content: string;
57 tags?: string[]; // Optional
58 readonly author: string;
59}
60
61const 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-only
68
69// Index signatures — dynamic keys
70interface StringMap {
71 [key: string]: string;
72}
73const headers: StringMap = {
74 "Content-Type": "application/json",
75 "Authorization": "Bearer token123"
76};

🏋️ Practice Exercise

Mini Exercise:

  1. Create an interface User and a type User — observe that both work for object shapes
  2. Extend an interface with extends and create an equivalent using & intersection
  3. Use declaration merging to add a custom property to the global Window interface
  4. Create a type alias for a union of string literals (e.g., HTTP methods)
  5. Create an interface with readonly and 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) when extends would give clearer error messages — intersection conflicts produce confusing types

  • Confusing readonly with deep immutability — readonly only prevents reassignment of the property itself, not mutation of nested objects

  • Over-using index signatures [key: string]: any — this weakens type safety; use Record<string, SpecificType> or a proper interface instead

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Interfaces vs Type Aliases