env.dev

Constants in JavaScript & TypeScript: const & as const

JS const blocks reassignment, not mutation. Real immutability comes from Object.freeze (ES5) or — better — TypeScript's as const assertion (TS 3.4, 2019) with zero runtime cost.

Last updated:

JavaScript got const in 2015 with ES6, and the rule it enforces is narrow: the binding cannot be reassigned. The value can still mutate. A const object is freely mutable. A const array can be pushed to. For real immutability you reach for Object.freeze() (ES5, 2009) or — the modern winner — TypeScript's as const assertion (TS 3.4, 2019), which gives compile-time readonly types with zero runtime cost. This guide covers all three plus the built-in numeric and Math constants every JS engineer should know cold.

What does const actually guarantee?

const is a block-scoped declaration that prevents reassignment of the identifier. It must be initialized at the declaration site — there is no separate "declare then assign" pattern. After that, the binding is permanent within its block; the value at the other end of the binding is not.

javascript
const MAX_RETRIES = 3;
const API_BASE_URL = 'https://api.example.com/v1';
const TIMEOUT_MS = 30_000;

// MAX_RETRIES = 5;
// → TypeError: Assignment to constant variable

const config = { timeout: 5000 };
config.timeout = 10_000;   // OK — mutating a property
config.added = true;       // OK — adding a property
// config = {};            // TypeError — rebinding

The rule of thumb: use const by default. Reach for let only when you genuinely need reassignment. Never use var in modern code — its function-scoping and hoisting behaviour are what let and const were introduced to replace.

How do you make an object truly immutable?

Object.freeze() makes an object's own properties read-only. In strict mode (the default in ES modules), an attempted write throws TypeError. In sloppy mode, the write is silently dropped — which is the worse of the two failure modes because the bug only surfaces under conditions you do not control.

javascript
const CONFIG = Object.freeze({
  maxUploadSize: 10 * 1024 * 1024, // 10 MiB
  allowedFormats: Object.freeze(['png', 'jpg', 'webp']),
  api: Object.freeze({
    baseUrl: 'https://api.example.com',
    timeout: 5000,
  }),
});

// CONFIG.maxUploadSize = 0;          // TypeError in strict mode
// CONFIG.allowedFormats.push('gif'); // TypeError — inner array is frozen too

Object.freeze() is shallow. Nested objects must be frozen individually, or you write a recursive deep-freeze utility. In production codebases the runtime cost of freeze is usually irrelevant, but the mental cost of remembering "freeze every level" is enough that most teams now prefer TypeScript's as const for compile-time immutability.

Why is "as const" the modern winner?

TypeScript's as const assertion (added in TS 3.4) tells the compiler to infer the narrowest possible type for a literal. Object properties become readonly with literal types. Arrays become readonly tuples. There is zero runtime cost — it is purely a compile-time check that the rest of your code will not mutate the value.

typescript
const HTTP_STATUS = {
  Ok: 200,
  Created: 201,
  BadRequest: 400,
  NotFound: 404,
  ServerError: 500,
} as const;
// type: { readonly Ok: 200; readonly Created: 201; ... }

type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
// 200 | 201 | 400 | 404 | 500

const DIRECTIONS = ['north', 'south', 'east', 'west'] as const;
type Direction = typeof DIRECTIONS[number];
// 'north' | 'south' | 'east' | 'west'

Pair as const with satisfies (TS 4.9, 2022) when you want both the literal narrowing and a structural check against an interface:

typescript
type Theme = { primary: string; spacing: number };

const THEME = {
  primary: '#6366f1',
  spacing: 8,
} as const satisfies Theme;
// THEME.primary still has the literal type "#6366f1"
// — but TypeScript verifies the shape matches Theme

"as const" or enum?

TypeScript enum emits runtime code (a reverse-mapped object), which defeats tree-shaking and adds bytes to your bundle. The official TS team has nudged toward as const for years, and TS 5.0 (2023) added const enum inlining as a partial mitigation, but as const remains the cleaner default for new code.

typescript
// enum — generates runtime code
enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }

// as const — purely compile-time, tree-shakeable
const DIRECTION = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;
type Direction = typeof DIRECTION[keyof typeof DIRECTION];

Which built-in constants should you know?

JavaScript exposes a handful of numeric limits and mathematical constants on Number and Math. The two most footgun-prone are Number.MAX_SAFE_INTEGER (2^53 − 1 = 9_007_199_254_740_991) — beyond it, integer arithmetic loses precision — and Number.EPSILON, the smallest gap between two representable doubles, used for safe float comparison.

javascript
// Numeric limits
Number.MAX_SAFE_INTEGER  // 9007199254740991  (use BigInt above this)
Number.MIN_SAFE_INTEGER  // -9007199254740991
Number.MAX_VALUE         // ~1.8e308
Number.MIN_VALUE         // ~5e-324  (smallest positive — not the most negative)
Number.EPSILON           // ~2.2e-16
Number.POSITIVE_INFINITY // Infinity
Number.NEGATIVE_INFINITY // -Infinity
Number.NaN               // NaN     (use Number.isNaN, not ===)

// Math constants
Math.PI    // 3.141592653589793
Math.E     // 2.718281828459045
Math.LN2   // 0.6931471805599453
Math.LN10  // 2.302585092994046
Math.SQRT2 // 1.4142135623730951

Number.MIN_VALUE is the most-misnamed constant in the language: it is the smallest positive double, not the most negative number. For the most-negative finite double, use -Number.MAX_VALUE.

Practical patterns and gotchas

  • Numeric separators: 1_000_000 beats 1000000, 0xFF_FF beats 0xFFFF. Shipping in V8 since 2019, in every modern engine.
  • Use === for NaN comparison? No. NaN === NaN is false. Use Number.isNaN(x).
  • Don't mutate frozen arrays at the type level: TypeScript's readonly tuples from as const reject .push() at compile time, which is usually what you want.
  • Module-level const is your config file: define shared constants in a dedicated module and import them. Don't repeat literal values across files — see constants best practices for naming and organization rules.
  • Avoid TypeScript enum in new code unless your team already uses it heavily. as const + a derived union type is the modern idiom and tree-shakes cleanly.

For language-agnostic naming, immutability, and "magic number" rules, see constants best practices and constants tips & tricks.

Was this helpful?

Read next

Node.js Env Variables: process.env, dotenv & --env-file

How to use environment variables in Node.js: process.env, dotenv, the Node 20.6+ --env-file flag, NODE_ENV, type-safe validation with zod.

Continue →

Frequently Asked Questions

Does const make a JavaScript object immutable?

No. const blocks reassignment of the binding, not mutation of the value. A const object can have its properties changed; a const array can be pushed to. For real immutability use Object.freeze (ES5, 2009) or TypeScript's as const assertion (TS 3.4, 2019).

Should I use TypeScript enum or "as const"?

Prefer as const for new code. enum emits runtime code (a reverse-mapped object) which defeats tree-shaking. as const with a derived union type is purely compile-time, tree-shakes cleanly, and is the modern TS idiom.

What is the difference between Number.MIN_VALUE and Number.MIN_SAFE_INTEGER?

Number.MIN_VALUE is the smallest positive double (~5e-324) — it is the most-misnamed constant in the language. For the most-negative finite double use -Number.MAX_VALUE. Number.MIN_SAFE_INTEGER (-9_007_199_254_740_991) is the lower bound for integer precision; below that, integer arithmetic loses precision.

When should I use "as const satisfies"?

When you want both the literal type narrowing of as const and a structural check against an interface. const THEME = { primary: "#6366f1" } as const satisfies Theme — TypeScript verifies the shape matches Theme but THEME.primary still has the literal type "#6366f1". Available since TS 4.9 (Nov 2022).

Stay up to date

Get notified about new guides, tools, and cheatsheets.