Const Assertion (as const)
A const assertion, written as const, asks TypeScript to infer the narrowest possible type for a value and to make it deeply readonly. Where normal inference widens a string to string and an array to a mutable T[], as const freezes everything: string and number literals stay as their exact literal types, object properties become readonly, and arrays become readonly tuples. This is enormously useful for building configuration objects, action constants, and lookup tables that you want the type system to treat as fixed, precise data rather than loosely-typed mutable structures.
Literal narrowing instead of widening
By default TypeScript widens literals so the variable can be reassigned. as const opts out of that widening, preserving the literal type.
let a = "GET"; // type: string (widened)
const b = "GET"; // type: "GET" (const keeps literals)
let c = "GET" as const; // type: "GET" (even with let)
let method = "GET" as const;
// ❌ Error: Type '"POST"' is not assignable to type '"GET"'.
// method = "POST";
Note the difference between the const keyword and the as const assertion. The keyword narrows only the immediate binding; as const narrows recursively through objects and arrays, which the keyword alone does not do.
Deeply readonly objects
Applied to an object literal, as const makes every property readonly and infers each value as its literal type rather than its general type.
const config = {
endpoint: "/api/v1",
retries: 3,
features: { darkMode: true },
} as const;
// Inferred type:
// {
// readonly endpoint: "/api/v1";
// readonly retries: 3;
// readonly features: { readonly darkMode: true };
// }
// ❌ Error: Cannot assign to 'retries' because it is a read-only property.
// config.retries = 5;
Without as const, endpoint would be string, retries would be number, and every property would be mutable. The assertion turns the object into a precise, immutable description of its own contents.
Arrays become readonly tuples
On an array literal, as const produces a readonly tuple with a fixed length and exact element types — perfect for deriving union types.
const ROLES = ["admin", "editor", "viewer"] as const;
// type: readonly ["admin", "editor", "viewer"]
type Role = (typeof ROLES)[number];
// type: "admin" | "editor" | "viewer"
function setRole(role: Role) { /* ... */ }
setRole("editor"); // ✅
// ❌ Error: Argument of type '"guest"' is not assignable to parameter of type 'Role'.
// setRole("guest");
This “array of values → union of literals” pattern is one of the most valuable uses of as const: a single source of truth that is both a runtime array you can iterate and a compile-time union you can type against.
as const vs plain inference
The differences between ordinary inference and a const assertion are easiest to see side by side.
| Value | Default inference | With as const |
|---|---|---|
"hello" | string | "hello" |
42 | number | 42 |
[1, 2] | number[] | readonly [1, 2] |
{ x: 1 } | { x: number } | { readonly x: 1 } |
{ k: "a" } | { k: string } | { readonly k: "a" } |
The pattern is consistent: literals stay literal, collections become readonly, and structure is preserved exactly.
A practical pattern: typed action constants
Const assertions shine when you want runtime constants and their derived types to stay in sync automatically — a classic need in reducers and event systems.
const ACTIONS = {
increment: "counter/increment",
decrement: "counter/decrement",
} as const;
type ActionType = (typeof ACTIONS)[keyof typeof ACTIONS];
// type: "counter/increment" | "counter/decrement"
function dispatch(type: ActionType) { /* ... */ }
dispatch(ACTIONS.increment); // ✅
Add or rename an action in the object and the ActionType union updates with zero extra work — no hand-maintained enum or duplicated string literals.
as constis a type-level freeze only — it is erased at runtime. The object is not actually frozen, so a deliberate cast could still mutate it. For runtime immutability useObject.freeze, whichas constcomplements but does not replace.
Single elements and enum alternatives
Because a union of literals derived via as const carries no runtime code beyond the array itself, it is often a leaner alternative to a TypeScript enum, which emits a runtime object.
// enum emits runtime JS:
enum Status { Active = "active", Inactive = "inactive" }
// as const union — no extra runtime object:
const STATUS = ["active", "inactive"] as const;
type Status2 = (typeof STATUS)[number]; // "active" | "inactive"
Both express the same finite set of values; the as const version is tree-shakeable and keeps the values as plain strings.
Best Practices
- Use
as constto derive literal-union types from a single runtime array or object. - Prefer the
(typeof ARR)[number]pattern over hand-written, duplicated literal unions. - Apply
as constto configuration and lookup objects you intend to treat as immutable. - Remember it widens nothing — combine with
Object.freezewhen you need runtime immutability too. - Reach for
as constunions as a lightweight, tree-shakeable alternative toenum. - Don’t expect mutation methods on the result; arrays become
readonlytuples. - Distinguish the
constkeyword (shallow, binding-level) fromas const(deep, recursive).