Constants #

Constants are values that don’t change during an application’s lifetime — login attempt limits, API URLs, theme color codes, environment names, and the like. In JavaScript, we’re used to using const for these. But TypeScript opens a richer new dimension: besides protecting the variable binding the way const does in JavaScript, TypeScript can guarantee that a value’s contents truly can’t change through Readonly<T>, Object.freeze, and as const. Understanding the difference between “the binding can’t change” and “the value can’t change” is the key to writing constants that are truly constant.

const — Binding Protection, Not Value Protection #

const is the most common keyword for declaring constants. It ensures a variable can’t be reassigned to another value after its initial declaration. But it’s important to understand: const only protects the binding (the relationship between the variable name and the value it points to), not the contents of that value.

// Primitives with const — truly cannot change
const PI: number = 3.14159265358979;
const NAMA_PERUSAHAAN: string = "MuslimApps";
const MAKS_PERCOBAAN: number = 3;
const FITUR_AKTIF: boolean = true;

// Cannot be reassigned
// PI = 3.14;           // ✗ Error: Cannot assign to 'PI' because it is a constant
// NAMA_PERUSAHAAN = ""; // ✗ Error: Cannot assign to 'NAMA_PERUSAHAAN' because it is a constant

For primitive values (number, string, boolean), const already provides full guarantees — the value can’t change at all. Problems arise when the constant is an object or array.

const with Objects — The Immutability Myth #

This is one of the most common misconceptions about const:

const konfigurasi = {
  host: "localhost",
  port: 5432,
  ssl: false,
};

// ANTI-PATTERN: Assuming a const object can't be modified
// In fact, object properties CAN be changed even when the variable is const
konfigurasi.port = 9999;  // ✓ No error — this is ALLOWED by TypeScript
konfigurasi.ssl = true;   // ✓ No error — this is ALLOWED by TypeScript

// What you can't do is reassign to a new object
// konfigurasi = { host: "prod.db.com", port: 5432, ssl: true }; // ✗ Error

In other words, code like the following is completely legal even though the variable is const:

const pengguna = { nama: "Budi", peran: "user" };

function tingkatkanPeran(p: typeof pengguna) {
  p.peran = "admin"; // Modifying the object received as an argument — possible!
}

tingkatkanPeran(pengguna);
console.log(pengguna.peran); // "admin" — the object has changed!

Readonly<T> — Immutability at the Type Level #

Readonly<T> is a built-in TypeScript utility type that makes all properties of a type readonly — the compiler will reject any attempt to modify those properties.

// Without Readonly — properties can be modified
const konfigDB = {
  host: "localhost",
  port: 5432,
};
konfigDB.port = 9999; // ✓ Allowed — not safe for configuration

// With Readonly — the compiler rejects property modifications
const konfigDBSafe: Readonly<{
  host: string;
  port: number;
}> = {
  host: "localhost",
  port: 5432,
};

// konfigDBSafe.port = 9999; // ✗ Error: Cannot assign to 'port' because it is a read-only property

Readonly<T> can also be applied to interfaces:

interface KonfigurasiAplikasi {
  readonly namaAplikasi: string;
  readonly versi: string;
  readonly urlApi: string;
  readonly batasRequest: number;
}

const KONFIGURASI: KonfigurasiAplikasi = {
  namaAplikasi: "MuslimApps",
  versi: "2.1.0",
  urlApi: "https://api.muslimapps.id",
  batasRequest: 100,
};

// KONFIGURASI.urlApi = "https://hack.com"; // ✗ Error — protected by readonly

ReadonlyArray<T> — Arrays That Can’t Be Modified #

The Readonly version for arrays prevents modifying operations like push, pop, splice, and the like:

const DAFTAR_BAHASA: ReadonlyArray<string> = ["id", "en", "ar"];
// Or with alternative syntax:
const DAFTAR_ZONA: readonly string[] = ["Asia/Jakarta", "Asia/Makassar", "Asia/Jayapura"];

// DAFTAR_BAHASA.push("fr"); // ✗ Error: Property 'push' does not exist on type 'readonly string[]'
// DAFTAR_BAHASA[0] = "ms";  // ✗ Error: Index signature in type 'readonly string[]' only permits reading

// Reading is allowed
console.log(DAFTAR_BAHASA[0]); // "id"
console.log(DAFTAR_BAHASA.length); // 3
Readonly<T> is only shallow — it protects first-level properties, but not the properties of nested objects. If you need deep immutability at the TypeScript level, you need to create your own DeepReadonly type or use a library like ts-essentials.

Object.freeze — Immutability at the Runtime Level #

Readonly<T> only provides protection at the TypeScript level — after being compiled to JavaScript, nothing prevents JS code from modifying the object. Object.freeze() provides real protection at the runtime level: a frozen object cannot be modified at all, even in plain JavaScript.

const BATAS_SISTEM = Object.freeze({
  maxPenggunaPerHari: 10000,
  maxFileUpload: 10,          // in MB
  maxDurasiSesi: 3600,        // in seconds
  maxPercobaan: 3,
});

// At the TypeScript level — TypeScript infers all properties as readonly
// BATAS_SISTEM.maxPercobaan = 5; // ✗ Compilation error

// At the runtime level — Object.freeze prevents modification in JavaScript
// Even JS code that doesn't go through TypeScript can't change it

Combining Object.freeze with an explicit type is the most defensive approach:

interface KonfigPembayaran {
  readonly providerAktif: string[];
  readonly batasTransaksiHarian: number;
  readonly matauang: string;
}

const KONFIG_PEMBAYARAN: Readonly<KonfigPembayaran> = Object.freeze({
  providerAktif: ["midtrans", "xendit", "gopay"],
  batasTransaksiHarian: 50_000_000,
  matauang: "IDR",
});

Comparing Immutability Approaches #

ApproachCompile-time ProtectionRuntime ProtectionDeep?
const aloneBinding onlyNone
Readonly<T>All propertiesNoneShallow
Object.freeze()NoneYes (shallow)Shallow
Readonly<T> + freezeAll propertiesYes (shallow)Shallow

as const — Const Assertions #

as const is a very powerful TypeScript feature for constants. It tells the compiler to treat the value as precisely as possible — turning the type into the most specific literal type and making all properties readonly at once.

// Without as const — TypeScript infers a wide type
const titikTanpa = { x: 10, y: 20 };
// Type: { x: number; y: number } — can hold any number

// With as const — TypeScript infers the most specific literal type
const titikDengan = { x: 10, y: 20 } as const;
// Type: { readonly x: 10; readonly y: 20 } — values can only be 10 and 20

// titikDengan.x = 99; // ✗ Error: Cannot assign to 'x' because it is a read-only property

as const is very useful for defining constants whose values will be used as literal types:

// Without as const — the direction type is string, too wide
const ARAH_TANPA = {
  ATAS: "atas",
  BAWAH: "bawah",
  KIRI: "kiri",
  KANAN: "kanan",
} ;
type ArahTanpa = typeof ARAH_TANPA[keyof typeof ARAH_TANPA]; // string

// With as const — the type is a precise literal union
const ARAH = {
  ATAS: "atas",
  BAWAH: "bawah",
  KIRI: "kiri",
  KANAN: "kanan",
} as const;

type Arah = typeof ARAH[keyof typeof ARAH];
// Type: "atas" | "bawah" | "kiri" | "kanan"

function pindah(arah: Arah): void {
  console.log(`Bergerak ke ${arah}`);
}

pindah(ARAH.ATAS);   // ✓ "atas"
pindah("kiri");      // ✓ valid literal string
// pindah("diagonal"); // ✗ Error: Argument of type '"diagonal"' is not assignable

as const for Arrays #

// Without as const — the type is string[]
const METODE_PEMBAYARAN_TANPA = ["transfer", "kartu_kredit", "dompet_digital"];
// Type: string[]

// With as const — the type is a readonly literal tuple
const METODE_PEMBAYARAN = ["transfer", "kartu_kredit", "dompet_digital"] as const;
// Type: readonly ["transfer", "kartu_kredit", "dompet_digital"]

type MetodePembayaran = typeof METODE_PEMBAYARAN[number];
// Type: "transfer" | "kartu_kredit" | "dompet_digital"

Literal Types as Constants #

Literal types let you define a variable whose type is that specific value itself — not a general category like string or number, but the exact value:

// Literal type — the variable can only hold exactly the specified value
const STATUS_AKTIF: "aktif" = "aktif";
const VERSI: 2 = 2;
const DEBUG_MODE: false = false;

// STATUS_AKTIF = "nonaktif"; // ✗ Error: Type '"nonaktif"' is not assignable to type '"aktif"'

Literal types are more useful when combined with unions to define a set of valid values:

// Literal union as a type constant
type Environment = "development" | "staging" | "production";
type LogLevel = "debug" | "info" | "warn" | "error";
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// A function with a parameter restricted to literal values
function setEnvironment(env: Environment): void {
  console.log(`Environment diset ke: ${env}`);
}

setEnvironment("production"); // ✓
// setEnvironment("testing");    // ✗ Error: Argument of type '"testing"' is not assignable to parameter of type 'Environment'

Enums as Constant Groups #

Enums are TypeScript’s way of grouping related constants into a single named unit. Enums produce real JavaScript code when compiled (unlike type or interface).

// Numeric enum — automatic numeric values
enum PrioritasTiket {
  Rendah = 1,
  Sedang = 2,
  Tinggi = 3,
  Kritis = 4,
}

// String enum — more descriptive and easier to debug
enum StatusTransaksi {
  Menunggu    = "MENUNGGU",
  Diproses    = "DIPROSES",
  Berhasil    = "BERHASIL",
  Gagal       = "GAGAL",
  Dibatalkan  = "DIBATALKAN",
}

function prosesTransaksi(status: StatusTransaksi): string {
  switch (status) {
    case StatusTransaksi.Berhasil:
      return "Transaksi selesai, terima kasih!";
    case StatusTransaksi.Gagal:
      return "Transaksi gagal, silakan coba lagi";
    case StatusTransaksi.Dibatalkan:
      return "Transaksi dibatalkan";
    default:
      return `Status: ${status}`;
  }
}

const enum — Compiler-Inlined Enums #

const enum is a more efficient enum variant — the compiler replaces every enum usage with its literal value directly, so no enum object exists in the JavaScript output:

// const enum — doesn't produce a JavaScript object
const enum KodeHTTP {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalError = 500,
}

function tanganiResponse(kode: KodeHTTP): string {
  if (kode === KodeHTTP.OK) return "Sukses";
  if (kode === KodeHTTP.NotFound) return "Tidak ditemukan";
  return "Error tidak diketahui";
}

// After compilation, the code above becomes:
// if (kode === 200) return "Sukses";
// if (kode === 404) return "Tidak ditemukan";
// The enum values are substituted directly — more efficient at runtime

When to Use Enum vs Literal Union #

Use Enum if:
  ✓ You need iteration (Object.values(MyEnum))
  ✓ You need reverse mapping (value → name)
  ✓ You're creating meaningful numeric constants
  ✓ The code is shared with non-TypeScript projects

Use Literal Union if:
  ✓ You don't need iteration or reverse mapping
  ✓ You want a lighter type (no JS output)
  ✓ It needs to compose easily with other types
  ✓ It works within the same file or a small module

Organizing Constants in Modules #

Real projects usually have many constants that need to be well organized. The recommended approach is grouping them into dedicated modules by domain:

// src/constants/api.ts
export const API = {
  BASE_URL: "https://api.muslimapps.id",
  VERSION: "v2",
  TIMEOUT: 30_000, // 30 seconds in milliseconds
  ENDPOINTS: {
    AUTH: "/auth",
    PROFIL: "/profil",
    JADWAL_SHOLAT: "/jadwal-sholat",
    KIBLAT: "/kiblat",
  },
} as const;

export type ApiEndpoint = typeof API.ENDPOINTS[keyof typeof API.ENDPOINTS];
// src/constants/ui.ts
export const UI = {
  ANIMASI_DURASI: 300,  // ms
  DEBOUNCE_DELAY: 500,  // ms
  MAKS_KARAKTER_BIO: 160,
  UKURAN_HALAMAN_DEFAULT: 20,
} as const;

export const WARNA = {
  HIJAU_PRIMER: "#1a7f5a",
  EMAS: "#c9a84c",
  GELAP: "#1a1a2e",
  TERANG: "#f8f9fa",
} as const;
// src/constants/validasi.ts
export const VALIDASI = {
  PANJANG_PASSWORD_MIN: 8,
  PANJANG_NAMA_MIN: 2,
  PANJANG_NAMA_MAX: 100,
  MAKS_UKURAN_FOTO: 5 * 1024 * 1024, // 5 MB in bytes
  FORMAT_TELEPON: /^(\+62|0)[0-9]{9,12}$/,
} as const;
// src/constants/index.ts — re-export everything from a single point
export * from "./api";
export * from "./ui";
export * from "./validasi";

With this structure, imports elsewhere become clean:

// In other components or services
import { API, VALIDASI, WARNA } from "@/constants";

function ambilJadwalSholat(kota: string) {
  return fetch(`${API.BASE_URL}${API.VERSION}${API.ENDPOINTS.JADWAL_SHOLAT}?kota=${kota}`, {
    signal: AbortSignal.timeout(API.TIMEOUT),
  });
}

Choosing a Constants Approach #

flowchart TD
    A{What kind of value\\nis your constant?} --> B[Primitive\\nnumber, string, boolean]
    A --> C[Object / Array]
    A --> D[A group of\\nrelated values]

    B --> B1[const with\\nSCREAMING_SNAKE_CASE]

    C --> E{Does it need to be\\ntruly immutable?}
    E -- No --> F[Plain const]
    E -- Yes, compile-time only --> G[Readonly + as const]
    E -- Yes, runtime too --> H[Object.freeze\\n+ Readonly]

    D --> I{Need iteration or\\nreverse mapping?}
    I -- Yes --> J[Enum or const enum]
    I -- No --> K[Literal union type\\nor as const object]

    style B1 fill:#51cf66,color:#fff
    style F fill:#339af0,color:#fff
    style G fill:#339af0,color:#fff
    style H fill:#51cf66,color:#fff
    style J fill:#fcc419,color:#000
    style K fill:#51cf66,color:#fff

Summary #

  • const only protects the binding — it prevents reassignment, but object properties and array elements can still change; don’t be fooled into thinking const = immutable.
  • Readonly<T> for immutability at the type level — all properties become readonly and the compiler rejects modification; but this protection only exists at compile time, not at runtime.
  • Object.freeze() for runtime immutability — provides real protection in JavaScript; combine it with Readonly<T> for double protection at compile time and runtime.
  • as const is the most powerful tool for constants — it narrows the type to the most specific literal, makes all properties readonly, and enables extracting union types from constant values.
  • Enums suit constant groups that need iteration or have meaningful numeric values; for simple string values, literal unions or as const objects are lighter and more flexible.
  • const enum is more efficient than a regular enum — values are inlined directly by the compiler, producing no JavaScript object in the output, so the bundle size is smaller.
  • Organize constant modules — group constants by domain (api.ts, ui.ts, validasi.ts) and re-export from index.ts; this makes imports easier and prevents “magic numbers” from being scattered across the codebase.
  • Use SCREAMING_SNAKE_CASE for global constants that truly never change; this gives a clear visual signal that the value shouldn’t be touched.

← Previous: Variables   Next: Data Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact