TypeScript Readonly & Const Assertions

Create immutable values and types

🔒 What are Readonly & Const Assertions?

Readonly and const assertions make values immutable in TypeScript. Readonly prevents property modifications, while const assertions (as const) create the most specific literal types possible, making values deeply immutable.


const config = { api: "https://api.com", timeout: 5000 } as const;
// config.timeout = 3000;  // ✗ Error: Cannot assign to readonly
                                    

Key Concepts

🔐

readonly Modifier

Prevent property reassignment

type User = {
  readonly id: number;
  name: string;
};
📌

as const

Create literal types

const colors = ["red", "blue"] as const;
// Type: readonly ["red", "blue"]
🛡️

Deep Immutability

Make nested objects readonly

const config = {
  db: { host: "localhost" }
} as const;
🎯

Literal Types

Narrow types to exact values

const status = "active" as const;
// Type: "active" (not string)

🔹 readonly Property Modifier

Prevent property modifications in types:

type User = {
  readonly id: number;
  name: string;
  readonly email: string;
};

const user: User = {
  id: 1,
  name: "Alice",
  email: "[email protected]"
};

user.name = "Bob";      // ✓ Valid: name is not readonly
// user.id = 2;         // ✗ Error: Cannot assign to readonly property
// user.email = "new";  // ✗ Error: Cannot assign to readonly property

console.log(user.id);   // ✓ Can read readonly properties

🔹 Readonly Arrays

Prevent array modifications:

// Using ReadonlyArray type
const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];

console.log(numbers[0]);     // ✓ Can read
// numbers[0] = 10;          // ✗ Error: Index signature is readonly
// numbers.push(6);          // ✗ Error: push doesn't exist on readonly array
// numbers.pop();            // ✗ Error: pop doesn't exist

// Using readonly modifier (shorter syntax)
const colors: readonly string[] = ["red", "green", "blue"];

console.log(colors[1]);      // ✓ Can read
// colors[1] = "yellow";     // ✗ Error: Cannot assign
// colors.push("purple");    // ✗ Error: push doesn't exist

🔹 Const Assertions Basics

Create literal types with as const:

// Without as const
const name1 = "Alice";        // Type: string
const age1 = 30;              // Type: number
const active1 = true;         // Type: boolean

// With as const
const name2 = "Alice" as const;   // Type: "Alice"
const age2 = 30 as const;         // Type: 30
const active2 = true as const;    // Type: true

// For objects
const user = {
  name: "Bob",
  age: 25
} as const;
// Type: { readonly name: "Bob"; readonly age: 25 }

// user.name = "Charlie";  // ✗ Error: Cannot assign to readonly

🔹 Const Assertions with Arrays

Create readonly tuple types:

// Without as const
const colors1 = ["red", "green", "blue"];
// Type: string[]

// With as const
const colors2 = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]

// colors2[0] = "yellow";  // ✗ Error: Cannot assign
// colors2.push("purple"); // ✗ Error: push doesn't exist

// Use in function parameters
function printColor(color: typeof colors2[number]) {
  console.log(color);
}

printColor("red");      // ✓ Valid
printColor("green");    // ✓ Valid
// printColor("yellow"); // ✗ Error: not in tuple

🔹 Deep Readonly with Const Assertions

Make nested objects immutable:

const config = {
  api: {
    url: "https://api.example.com",
    timeout: 5000,
    endpoints: {
      users: "/users",
      posts: "/posts"
    }
  },
  features: {
    darkMode: true,
    notifications: false
  }
} as const;

// All properties are deeply readonly
console.log(config.api.url);  // ✓ Can read

// config.api.timeout = 3000;              // ✗ Error
// config.features.darkMode = false;       // ✗ Error
// config.api.endpoints.users = "/people"; // ✗ Error

🔹 Const Assertions for Enums

Create enum-like objects with const assertions:

// Instead of enum
const Status = {
  PENDING: "pending",
  APPROVED: "approved",
  REJECTED: "rejected"
} as const;

// Extract the type
type StatusType = typeof Status[keyof typeof Status];
// Type: "pending" | "approved" | "rejected"

function updateStatus(status: StatusType) {
  console.log(`Status updated to: ${status}`);
}

updateStatus(Status.PENDING);    // ✓ Valid
updateStatus("approved");        // ✓ Valid
// updateStatus("unknown");      // ✗ Error: not a valid status

🔹 Readonly vs Const Assertions

Understand the differences:

// readonly: Type-level immutability
type Config1 = {
  readonly url: string;
  readonly port: number;
};

const config1: Config1 = {
  url: "localhost",
  port: 3000
};
// config1.port = 8080;  // ✗ Error

// as const: Value-level immutability + literal types
const config2 = {
  url: "localhost",
  port: 3000
} as const;
// Type: { readonly url: "localhost"; readonly port: 3000 }

// config2.port = 8080;  // ✗ Error
// config2 has literal types, config1 has general types

🔹 Practical Example: Configuration

Build type-safe, immutable configuration:

const APP_CONFIG = {
  app: {
    name: "MyApp",
    version: "1.0.0",
    environment: "production"
  },
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3
  },
  features: {
    authentication: true,
    analytics: true,
    darkMode: false
  },
  routes: {
    home: "/",
    login: "/login",
    dashboard: "/dashboard"
  }
} as const;

// Extract types
type AppConfig = typeof APP_CONFIG;
type Environment = typeof APP_CONFIG.app.environment;
type Route = typeof APP_CONFIG.routes[keyof typeof APP_CONFIG.routes];

function navigate(route: Route) {
  console.log(`Navigating to: ${route}`);
}

navigate(APP_CONFIG.routes.home);      // ✓ Valid
navigate("/dashboard");                // ✓ Valid
// navigate("/unknown");               // ✗ Error

🔹 Readonly with Generics

Create generic readonly utilities:

// Deep readonly utility
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

type User = {
  id: number;
  profile: {
    name: string;
    settings: {
      theme: string;
      notifications: boolean;
    };
  };
};

type ReadonlyUser = DeepReadonly<User>;

const user: ReadonlyUser = {
  id: 1,
  profile: {
    name: "Alice",
    settings: {
      theme: "dark",
      notifications: true
    }
  }
};

// user.profile.settings.theme = "light";  // ✗ Error: deeply readonly

💡 Key Takeaways

  • readonly modifier prevents property reassignment
  • as const creates literal types and deep immutability
  • Use ReadonlyArray<T> or readonly T[] for immutable arrays
  • Const assertions are perfect for configuration objects
  • Combine with typeof and keyof for type extraction
  • Immutability helps prevent bugs and makes code more predictable

🧠 Test Your Knowledge

What does as const do?