Skip to main content

TS Ep 15: Immutability with `as const` & Const Assertions

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 15: This Article
Declaring a variable with const in JavaScript prevents reassigning the variable binding, but it does not make objects or arrays immutable. TypeScript’s as const assertion (Const Assertions) bridges this gap by locking down values into deeply immutable, narrow literal types at compile time.

1. The Shallow Nature of JavaScript const
#

In JavaScript, const only guarantees that a variable reference cannot be reassigned. The internal contents of an object or array remain completely mutable:

// Valid JavaScript! 'const' does NOT stop mutation:
const user = { role: "admin" };
user.role = "superadmin"; // Allowed!

const tags = ["js", "ts"];
tags.push("python");       // Allowed!

Because JavaScript objects are mutable, TypeScript defaults to Literal Widening for object properties (as covered in Episode 8), inferring user.role as string rather than "admin".


2. The 3 Transformation Rules of as const
#

When you append as const to an expression, TypeScript applies 3 strict transformations:

  1. No Literal Widening: Primitive literal values are NOT widened ("GET" stays "GET", not string).
  2. Deep readonly Objects: Object properties recursively receive readonly modifiers.
  3. Readonly Tuples: Array literals are converted into readonly fixed-length Tuples.
// Without 'as const'
const standardConfig = {
  endpoint: "https://api.acme.com",
  timeout: 5000,
  retries: [1, 2, 3],
};
/* Inferred Type:
{
  endpoint: string;
  timeout: number;
  retries: number[];
}
*/

// With 'as const' (Const Assertion)
const immutableConfig = {
  endpoint: "https://api.acme.com",
  timeout: 5000,
  retries: [1, 2, 3],
} as const;
/* Inferred Type:
{
  readonly endpoint: "https://api.acme.com";
  readonly timeout: 5000;
  readonly retries: readonly [1, 2, 3]; // Readonly Tuple!
}
*/

Attempting Mutations on as const Objects
#

// ❌ Compiler Error: Cannot assign to 'endpoint' because it is a read-only property.
// immutableConfig.endpoint = "https://other-api.com";

// ❌ Compiler Error: Property 'push' does not exist on type 'readonly [1, 2, 3]'.
// immutableConfig.retries.push(4);

3. as const vs Object.freeze()
#

It is vital to understand the difference between compile-time const assertions and runtime object freezing:

Featureas constObject.freeze()
Enforcement LayerCompile-Time (TypeScript)Runtime (JavaScript V8 Engine)
JS Code FootprintErased during compilation (0 bytes)Remains in compiled JS bundle
DepthDeep (Recursively freezes nested objects)Shallow (Only freezes top-level keys)
Array TransformationConverts T[] to readonly [T1, T2]Freezes array instance methods
// Combining both for ultimate runtime AND compile-time immutability:
const productionSettings = Object.freeze({
  env: "production",
  port: 8080,
} as const);

4. Deriving Union Types from as const Arrays (typeof ARRAY[number])
#

One of the most powerful architectural patterns in TypeScript is creating a single source of truth for runtime values, and deriving compile-time TypeScript Union Types directly from it.

The Single-Source-of-Truth Pattern
#

Instead of maintaining a separate type Role = "admin" | "user" | "guest" and a separate runtime array const ROLES = ["admin", "user", "guest"], define the array once with as const:

// 1. Define the single source of truth runtime array with 'as const'
export const SUPPORTED_ROLES = ["admin", "editor", "viewer"] as const;

// 2. Derive the TypeScript Union Type using Indexed Access:
// Inferred Type: "admin" | "editor" | "viewer"
export type Role = typeof SUPPORTED_ROLES[number];

// 3. Use the derived type in application functions!
function assignRole(user: string, role: Role) {
  console.log(`Assigned role ${role} to ${user}`);
}

assignRole("Alice", "admin");  // 🟢 Valid!

// ❌ Compiler Error: Argument of type '"superadmin"' is not assignable to parameter of type 'Role'.
// assignRole("Bob", "superadmin"); 

How typeof SUPPORTED_ROLES[number] Works
#

  1. typeof SUPPORTED_ROLES retrieves the tuple type: readonly ["admin", "editor", "viewer"].
  2. [number] indexes into the tuple using any number index, extracting the union of all element types: "admin" | "editor" | "viewer".

5. Deriving Union Types from as const Objects (typeof OBJECT[keyof typeof OBJECT])
#

You can also derive a union of values from an object dictionary:

export const LOG_LEVELS = {
  DEBUG: 10,
  INFO: 20,
  WARN: 30,
  ERROR: 40,
} as const;

// Derive Union of Object Keys: "DEBUG" | "INFO" | "WARN" | "ERROR"
export type LogLevelKey = keyof typeof LOG_LEVELS;

// Derive Union of Object Values: 10 | 20 | 30 | 40
export type LogLevelValue = typeof LOG_LEVELS[keyof typeof LOG_LEVELS];

function setThreshold(level: LogLevelValue) {
  console.log(`Setting log threshold to level number: ${level}`);
}

setThreshold(20); // Valid (INFO)
// setThreshold(99); // ❌ Error! 99 is not a valid LogLevelValue.

Module 1 Completion Milestone! 🎉
#

Congratulations! You have completed Module 1: Fundamental TypeScript Essentials!

Throughout these 15 episodes, you have mastered:

  • The tsc compiler pipeline, AST parsing, and strict tsconfig.json flags.
  • Primitive types, wrapper object traps, and strictNullChecks.
  • Literal Widening, Contextual Typing, and Type Annotations vs Inference.
  • Structural Typing, Excess Property Checks, and Index Signatures.
  • Arrays, Tuples, Labeled Tuples, and readonly collections.
  • any vs unknown, Top Types, and error: unknown catch blocks.
  • Set Theory mechanics of Unions (|) and Intersections (&).
  • Unit Literal Types and String Literal Unions.
  • Control Flow Analysis, typeof, instanceof, and in guards.
  • Custom Type Guards (arg is Type) and Assertion Functions.
  • Discriminated Unions (Sum Types) and eliminating impossible states.
  • Function Annotations, Call Signatures, and void return semantics.
  • Function Overloads and signature order.
  • The never Bottom Type ($\bot$) and Exhaustiveness Checking.
  • as const assertions and single-source-of-truth type derivation.

In the next module, Module 2: Object-Oriented TypeScript (Classes & OOP), we will explore TypeScript’s class extensions, access modifiers (public, private, protected), parameter properties, abstract classes, and the polymorphic this type!

typescript - This article is part of a series.
Part 15: This Article