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 ownDeepReadonlytype or use a library likets-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 #
| Approach | Compile-time Protection | Runtime Protection | Deep? |
|---|---|---|---|
const alone | Binding only | None | — |
Readonly<T> | All properties | None | Shallow |
Object.freeze() | None | Yes (shallow) | Shallow |
Readonly<T> + freeze | All properties | Yes (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:#fffSummary #
constonly protects the binding — it prevents reassignment, but object properties and array elements can still change; don’t be fooled into thinkingconst= 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 withReadonly<T>for double protection at compile time and runtime.as constis the most powerful tool for constants — it narrows the type to the most specific literal, makes all propertiesreadonly, 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 constobjects are lighter and more flexible.const enumis 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 fromindex.ts; this makes imports easier and prevents “magic numbers” from being scattered across the codebase.- Use
SCREAMING_SNAKE_CASEfor global constants that truly never change; this gives a clear visual signal that the value shouldn’t be touched.