as const: Const Assertions for Literal, Immutable Types
AI generated
type
TypeScript
Using as const Correctly
Const assertions for literal, immutable types

as const forces the compiler to infer the narrowest possible literal type for an expression instead of widening it to a broad type like string or number[]. The result is more precise types, without writing any extra type definition.

10 min read TypeScript Type System

1. What as const does to an expression

Without a const assertion, TypeScript infers the widest matching type for a literal: let status = "active" gets the type string, not the literal "active". That widening makes sense because a let variable could later be reassigned.

Adding as const after an expression tells the compiler to do exactly the opposite: infer the narrowest possible literal type and implicitly mark every contained property as readonly.

The effect is not limited to single values, it applies recursively to entire expression trees: arrays become readonly tuples with exact element types, objects get readonly properties with literal instead of widened values.


let a = "active";        // type: string
let b = "active" as const; // type: "active"

const status = { code: 200, label: "ok" };
// type: { code: number; label: string }

const statusConst = { code: 200, label: "ok" } as const;
// type: { readonly code: 200; readonly label: "ok" }

2. Array literals with as const become tuples, not Array

A plain array literal gets widened into a generic, mutable array type, such as string[] for a list of color names. The order and length of the elements is completely lost in the process.

With as const, the same literal instead becomes a readonly tuple with exact literal element types at every position. This is particularly useful when a fixed list of allowed values needs to serve as both a runtime value and a type source.

Using typeof together with index access [number], such a tuple can directly produce a union of all contained literal values, without repeating the list a second time as a type.


const themes = ["light", "dark", "system"] as const;
// type: readonly ["light", "dark", "system"]

type Theme = (typeof themes)[number];
// type: "light" | "dark" | "system"

function setTheme(theme: Theme) {
  document.documentElement.dataset.theme = theme;
}

3. Object literals: readonly properties and literal values

For objects, as const marks every property as readonly and, if the value is a primitive literal, infers an exact literal type instead of a widened type like number or string.

Nested objects and arrays inside the literal are also captured recursively: a multi-level configuration object becomes fully immutably typed without wrapping every level manually in Readonly.

This works well for constants that act as the single source of truth for allowed values, configuration, or routing tables, because typos in derived types show up immediately as compile errors.

4. as const with union types instead of enum

Many teams now use an object with as const instead of an enum to define a fixed set of values. The advantage: no extra runtime construct is generated the way numeric enums do, just a plain object with literal values.

From that object, both a union type of the keys and of the values can be derived via typeof and keyof, which in practice tends to be more flexible than a classic TypeScript enum with its own special-case rules.

Another advantage over string enums: the values remain plain, unnamed string literals that compare cleanly against external APIs, JSON payloads, or database values, without going through an enum detour first.


const OrderStatus = {
  Pending: "pending",
  Shipped: "shipped",
  Delivered: "delivered",
} as const;

type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
// type: "pending" | "shipped" | "delivered"

function isFinal(status: OrderStatus): boolean {
  return status === OrderStatus.Delivered;
}

5. Combining as const and satisfies

as const alone produces the narrowest possible type, but it does not check whether that type actually matches an expected shape. That is exactly what the satisfies operator adds: it validates an expression against a target type without widening the inferred literal type.

The combination { ... } satisfies Record as const is not valid in that order, which is why satisfies typically comes right after the object literal and as const is dropped once a satisfies constraint is already checking the structure.

In practice, satisfies replaces many cases where as const alone used to be applied to configuration objects, because it additionally raises an error the moment a key is missing or a value does not match the expected type.


type RouteConfig = { path: string; auth: boolean };

const routes = {
  dashboard: { path: "/dashboard", auth: true },
  login: { path: "/login", auth: false },
} satisfies Record<string, RouteConfig>;

// routes.dashboard.path stays the literal "/dashboard", not just string

6. Practical example: configuration objects and action types

In state management libraries like Redux, or in hand-rolled reducer implementations, as const is commonly used to produce action objects with a literal type field that the compiler can evaluate for discriminated unions inside a switch statement.

Without as const, the type field would widen to string, which means TypeScript could no longer perform exhaustive checking in the switch or infer specific payload types per case.

API client configurations with fixed endpoint paths benefit too: a single object defined with as const serves simultaneously as the runtime value used for HTTP calls and as the type source for every allowed route.


function increment(amount: number) {
  return { type: "increment", amount } as const;
}
function reset() {
  return { type: "reset" } as const;
}

type Action = ReturnType<typeof increment> | ReturnType<typeof reset>;

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "increment":
      return state + action.amount;
    case "reset":
      return 0;
  }
}

7. The limits of as const: no runtime guarantee

as const is purely a compile-time construct. At runtime, the object or array remains a perfectly ordinary, mutable JavaScript value. Only the compiler refuses assignments to properties marked as readonly.

Anyone who needs actual runtime protection against mutation, for example because an object could be changed by non-TypeScript code, additionally needs Object.freeze(), which genuinely prevents mutation at runtime, or at least throws in strict mode.

In practice, compile-time-only protection is enough for most internal use cases, because mutation attempts by your own TypeScript code already surface as errors during development, long before they could reach production.

8. as const, Readonly, and Object.freeze compared

Readonly<T> converts an existing type into a readonly variant, but only one level deep, and it does not automatically narrow literal values, whereas as const does both at once, directly on the expression itself.

Object.freeze() is purely runtime behavior with no effect on the types TypeScript infers, unless it is combined with a generic return type that also carries the readonly semantics into the type layer.

In production code, all three are frequently combined: as const for literal, immutable configuration in source, Readonly<T> for function parameters meant to prevent mutation, and Object.freeze() wherever actual runtime protection is required.

9. Common mistakes when using as const

A frequent mistake is applying as const to a value that is then still declared with let: the literal, readonly type then blocks any later assignment, even though let would otherwise allow it.

A second pitfall involves contextual typing: when an object created with as const is passed to a function expecting a wider, mutable type, TypeScript can reject the readonly type, since a readonly structure is not automatically assignable to a mutable one.

In these cases, an explicit type annotation at the target site helps, or deliberately dropping as const in favor of satisfies, which checks the structure without blocking later assignability to mutable types.

Aspect as const Readonly Object.freeze()
Applies to An expression, right at its definition An existing type A runtime object
Depth Recursive across the whole expression One level deep only One level deep only (without extra code)
Literal types Yes, prevents widening No, only changes mutability No, purely a runtime effect
Runtime effect None None Actually prevents mutation
Typical use Constant configuration, tuples, action types Function parameters, API signatures Security-sensitive, immutable objects

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

as const

Literal types

Narrowest inferred type, no widening

Recursive readonly

Applies to nested arrays and objects

Compile-time only

No runtime guarantee against mutation

enum alternative

Object plus as const instead of enum

11. FAQ: as const

1What is the difference between as const and a regular type annotation?
A regular type annotation assigns a given type without inferring it. as const instead infers the narrowest possible literal type directly from the expression, with no need to write that type by hand.
2Does as const make an object immutable at runtime?
No. as const operates purely at the type level. At runtime the object stays a normal, mutable JavaScript value unless Object.freeze() is applied as well.
3Why does an array with as const become a tuple instead of Array?
Without as const, TypeScript widens an array literal into a mutable array type with a shared element type. as const instead preserves length, order, and the literal type of every single position.
4Can I use as const and satisfies together?
They can be combined, though satisfies already checks the structure against a target type without widening literal types. In practice satisfies often replaces using as const alone for configuration objects.
5Is an object with as const a good alternative to enum?
Yes, in many cases. It produces no extra runtime construct the way a numeric enum does, and typeof combined with keyof directly yields a matching union type of the values.
6Why does TypeScript reject a later assignment after as const?
Because as const implicitly marks every property as readonly. Any assignment to such a property counts as a violation of the readonly rule, regardless of whether the variable was declared with let or const.
7How do I derive a union type from an as-const array?
Via typeof followed by index access [number] on the array. The result is a union of every literal element type in the tuple, without listing the values a second time by hand.
8Why does TypeScript sometimes refuse to pass an as-const object to a parameter?
Because a readonly type is not automatically assignable to a mutable structure. An explicit type annotation at the target, or dropping as const in favor of satisfies, resolves this.
9Does as const also work on nested objects?
Yes, the effect is recursive. Every level of a nested object or array literal is marked readonly and, where possible, gets literal instead of widened types.
10When should I use Object.freeze() instead of as const?
Whenever actual runtime protection against mutation is needed, for example because an object could also be changed by code outside type checking or by external libraries.