Beyond the naming and immutability rules, constants are where compiler optimizations and type-system tricks really earn their keep. A literal embedded in a Rust binary is faster than reading a config file. as const in TypeScript narrows a string to its exact literal type and unlocks branded-type safety. Bundlers like Vite, esbuild, and Rollup will tree-shake unused exports and eliminate code paths gated on constant booleans, so a build-time feature flag costs zero runtime bytes when disabled. This guide covers the mechanics — compile-time vs runtime, tree shaking, constant folding, type- safety patterns, configuration, and how constants improve test fixtures.
Compile-time vs runtime: why does it matter?
Compile-time constants are baked into the binary or bundle. Runtime constants are read once at startup and held in memory thereafter. They behave the same to your application code, but their performance and safety profile differs.
Compile-time: Rust const, Go const, C++ constexpr, Java static final primitives. The compiler verifies the value, inlines it, and (for languages with const evaluators) can run arbitrary computation at build time.
Runtime: every JavaScript const, Python module-level constant, anything read from os.environ or a config file. The "constant" is computed during program initialization and held thereafter — but a bug in the initialization code can crash startup, where a bug in a compile-time constant fails the build.
// Rust: const fn — arbitrary compile-time computation
const fn factorial(n: u32) -> u64 {
match n {
0 | 1 => 1,
_ => n as u64 * factorial(n - 1),
}
}
const FACT_10: u64 = factorial(10); // 3_628_800 — folded at compile time// TypeScript: every const is runtime — but bundlers fold pure expressions
const SECONDS_PER_DAY = 60 * 60 * 24;
// Vite/esbuild rewrites this to: const SECONDS_PER_DAY = 86400;How does tree shaking interact with constants?
Tree shaking is a bundler optimization that removes unused exports from your final bundle. For it to work reliably with constants, two conditions matter:
- ES module exports, not CommonJS. CJS exports are dynamic, so bundlers cannot prove a name is unused.
- Pure top-level expressions. If your "constant" calls a function with side effects, the bundler must keep the call. Mark side-effect-free modules with
"sideEffects": falseinpackage.json.
// constants.ts — every export tree-shaken independently
export const FEATURE_A_ENABLED = true;
export const FEATURE_B_ENABLED = false;
export const MAX_FILE_SIZE = 10 * 1024 * 1024;
// consumer.ts — only imports A
import { FEATURE_A_ENABLED, MAX_FILE_SIZE } from './constants.ts';
// FEATURE_B_ENABLED is removed from the production bundle entirely.The killer use case is build-time feature flags. A constant false gating an if branch causes the bundler to eliminate the entire branch as dead code — disabled features add zero bytes to the production bundle. That is the cheapest possible feature flag for any deploy-to-toggle workflow.
What is constant folding?
Constant folding is a compiler optimization that evaluates expressions made entirely of constants at compile time, replacing the expression with its result. V8, esbuild, Rollup, swc, Java's javac, and every modern compiler do this aggressively for arithmetic and string concatenation. You can help the optimizer by expressing derived constants in terms of other constants using only operators it can fold:
// Folded: SECONDS_PER_DAY becomes the literal 86400 in the bundle
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = 24;
const SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY;
// Folded: 'https://' + 'api.example.com' + '/v1'
const API_URL = `${'https://'}${'api.example.com'}/v1`;
// NOT folded — function call with engine-specific behaviour
const KB = Math.pow(2, 10); // use 2 ** 10 or the literal 1024 insteadHow does "as const" enable type-safe constants?
TypeScript's as const narrows literals to their exact value, so downstream code can use the constant in places where the wide string / number would be rejected:
// Without as const — wide types
const ROLES = ['admin', 'editor', 'viewer'];
type Role = typeof ROLES[number]; // string
// With as const — literal types
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
function hasRole(role: Role) { /* ... */ }
hasRole('admin'); // OK
hasRole('superuser'); // Type error!Pair as const with satisfies (TS 4.9, November 2022) when you want both the literal narrowing and a structural check against an interface — useful for design tokens, config objects, and any place where you want the narrowest type that still conforms to a contract.
Branded types: stopping ID mix-ups at compile time
Two strings might both look like opaque tokens but represent fundamentally different things — a UserId and a ProductId, or a CustomerId and an InvoiceId. Branded types enforce the distinction in TypeScript at compile time with zero runtime cost:
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }
const userId = 'user_123' as UserId;
const productId = 'prod_456' as ProductId;
getUser(userId); // OK
getUser(productId); // Type error — cannot mix branded typesRust's newtype pattern (a tuple struct wrapping a primitive) achieves the same outcome with zero runtime overhead. Java enums and record types provide the analog when the brand is small enough to warrant a wrapper class.
Constants as configuration
Anything that changes rarely and ships with the build — feature flags, rate limits, pagination sizes, timeout durations, retry thresholds — is a constant in disguise. Centralize them in a config module rather than scattering literals through your codebase, and use SCREAMING_SNAKE_CASE with unit suffixes:
// config/limits.ts
export const MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50 MiB
export const MAX_ITEMS_PER_PAGE = 100;
export const DEFAULT_PAGE_SIZE = 25;
export const REQUEST_TIMEOUT_MS = 30_000;
export const MAX_RETRY_ATTEMPTS = 3;
export const RETRY_BACKOFF_BASE_MS = 1_000;For runtime-toggleable flags (enable a feature without redeploying), a feature-flag service is the right tool. But still define the flag names as constants so typos fail at compile time and you have a single inventory of "what flags exist".
Why constants make tests better
Tests benefit twice from well-defined constants. First, fixtures (the known input data and expected output) live in dedicated files where the test reads cleanly. Second, when you test boundary conditions — page-size limits, timeouts, retry counts — referencing the same constant the implementation uses means the test automatically tracks the limit when it changes:
// test/fixtures.ts — fixtures are constants, named with intent
export const TEST_USER = {
id: 'user_test_001',
email: 'test@example.com',
name: 'Test User',
role: 'editor',
} as const;
// In the test — uses the same constant as the implementation
import { MAX_ITEMS_PER_PAGE } from '../config/limits.ts';
test('paginates at the configured limit', () => {
const items = makeItems(MAX_ITEMS_PER_PAGE * 3);
const page = paginate(items);
expect(page.size).toBe(MAX_ITEMS_PER_PAGE);
});For HTTP status assertions, log levels, error codes — anything with semantic meaning — assert against the named constant, not the literal value. Refactoring the constant later does not break the test, and the assertion's intent is unmistakable.
Practical patterns
- Prefer compile-time over runtime. Rust
const fn, Goconst, TypeScriptas const— they cost zero at runtime and fail the build, not the deployment. - Use derived constants:
MAX_TOTAL = MAX_PER_PAGE * 10beats two unrelated literals. - Reach for branded types for ID-like values that share a primitive type but represent different things.
- Tree-shake your feature flags: a build-time
const FEATURE_X = false+ bundler dead-code elimination is the cheapest disabled-feature there is. - Test fixtures are constants: name them, type them with
as const, import them into the test.
For naming, immutability, and "magic number" rules, see constants best practices. For language-specific patterns, browse the per-language guides: JS/TS, Python, Go, Rust, Java.