TypeScript as const: Enforce literal and readonly types in config objects
Use TypeScript as const to preserve literal and readonly types in static config objects, avoid widening, and get safer narrowing without extra interfaces.
29 Aug 2025, 15:19 UTC

The problem shows up in a small config object. You write const cfg = { mode: 'dark', level: 3 } and expect TypeScript to remember that mode is exactly 'dark' and level is exactly 3. Instead the inferred type widens to { mode: string; level: number }. Widening erases the literal information you need for exhaustive checks, and the properties remain mutable. That is the gap as const closes.
Thesis: for static configuration, as const gives you literal types and deep readonly without writing an explicit interface. You keep the value as the source of truth and let the type follow it.
The widening problem in config objects
TypeScript widens primitive literals by default when inferring a const variable. Widening means 'dark' becomes string, 3 becomes number. That is useful for variables that will change, but harmful for config that should not.
Without literals, a switch on cfg.mode cannot be exhaustive. A function that overloads on 'dark' | 'light' will accept any string. Accidental mutation is also possible: cfg.mode = 'light' compiles.
What as const actually does
as const is a const assertion introduced in TypeScript 3.4. It tells the compiler to infer the narrowest possible type for the value and to treat nested objects and arrays as readonly.
Literal preservation
With the assertion, the type of { mode: 'dark', level: 3 } as const is { readonly mode: 'dark'; readonly level: 3 } rather than string and number. The literal is preserved, which improves type narrowing in conditionals and switches.
Deep readonly
The assertion is recursive. Object properties and array elements become readonly. The compiler will reject assignments to those properties. This is compile-time only; runtime mutation is still possible if code bypasses the type system.
Worked example: feature flags config
Consider a small feature flag object used across a module.
const FEATURES = {
search: true,
beta: false,
limits: { maxItems: 50, mode: 'strict' as const }
} as const;With as const the inferred type is a readonly literal structure. You can use it for safe selection:
type FeatureKey = keyof typeof FEATURES; // 'search' | 'beta' | 'limits'
type Mode = typeof FEATURES.limits.mode; // 'strict'Attempting FEATURES.search = false should produce a compiler error about assigning to a readonly property. You can verify the inference locally:
- Create a file
config.tsin your project root containing the declaration above. - Run
npx tsc --noEmit config.tsfrom a terminal with read access to the project. No special permissions are required. - Inspect the inferred type by hovering in your editor or by generating declaration output and checking that
modeis typed as the literal'strict'and properties are markedreadonly.
The risk here is that as const locks the shape. If the config needs to be updated at runtime, the assertion is the wrong tool.
Trade-off and when not to use
Overusing as const on large or frequently changing data makes code harder to maintain. The literal types become very specific, so refactoring requires updating every usage site. It also adds noise when the data is truly dynamic.
Because readonly is compile-time only, do not rely on it for security. Code that uses any or casts can still mutate the value at runtime.
Projects on TypeScript older than 3.4 cannot use const assertions and must upgrade first.
Actionable takeaway: use as const for small, static configuration objects where you want literal precision and accidental mutation prevention. Keep the value as the single source of truth, verify the literal inference in your editor, and avoid it for mutable or large data structures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.