What Does 'as const' Mean in TypeScript and When to Use It?

typescript as const const assertions literal types readonly

as const in TypeScript is a const assertion that tells the compiler to infer the narrowest possible type for a value. Instead of widening "hello" to string or [1, 2] to number[], TypeScript treats every property as its exact literal value and marks everything readonly. This gives you compile-time precision without explicit type declarations or enums, and it's the foundation for type-safe configuration objects, discriminated unions built from plain data, and tuple inference.

// Without as const: TypeScript widens to mutable, general types.
const status = { code: 200, message: "OK" };
// Type: { code: number; message: string }

// With as const: every value becomes a readonly literal.
const statusConst = { code: 200, message: "OK" } as const;
// Type: { readonly code: 200; readonly message: "OK" }

What Exactly Changes Under the Hood

A const assertion does three things at once. First, all string, number, and boolean values become literal types (200 instead of number). Second, every property in an object becomes readonly. Third, arrays become readonly tuples with fixed length and per-index literal types instead of a general Array<T>. This is recursive: nested objects and arrays all receive the same treatment.

// Arrays become readonly tuples.
const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]

// This means you can extract a union from the tuple.
type Color = (typeof colors)[number];
// Type: "red" | "green" | "blue"

Pattern 1: Replacing Enums with as const Objects

TypeScript enums have well-known quirks. Numeric enums allow reverse lookups that break type safety, and all enums emit runtime JavaScript that bloats your bundle. A const object with as const is a common alternative that is fully tree-shakeable and produces cleaner type inference. Libraries like Prisma and tRPC use this pattern.

const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

// Derive the union type from the object values.
type Direction = (typeof Direction)[keyof typeof Direction];
// Type: "UP" | "DOWN" | "LEFT" | "RIGHT"

function move(dir: Direction) {
  console.log(`Moving ${dir}`);
}

// Both work — you get autocomplete and type safety.
move(Direction.Up);
move("DOWN");

Pattern 2: Type-Safe Configuration and Route Maps

When you define a configuration object, you typically want TypeScript to know the exact keys and values so that downstream code gets type-checked against them. Without as const, TypeScript sees every value as string, and you lose the ability to discriminate between routes, event names, or API endpoints at the type level.

const routes = {
  home: "/",
  profile: "/profile",
  settings: "/settings",
} as const;

type RouteKey = keyof typeof routes;
// Type: "home" | "profile" | "settings"

type RoutePath = (typeof routes)[RouteKey];
// Type: "/" | "/profile" | "/settings"

function navigate(path: RoutePath) {
  window.location.href = path;
}

// Compile error: '"/nowhere"' is not assignable.
// navigate("/nowhere");
navigate(routes.profile);

Pattern 3: Discriminated Unions from Const Data

You can define a list of event shapes as a const array and derive a discriminated union from it. This lets you keep event definitions as plain data (useful for codegen or runtime validation) while still getting exhaustive type checking.

const events = [
  { type: "click", x: 0, y: 0 },
  { type: "keydown", key: "" },
] as const;

// Extract a union of the event types.
type EventType = (typeof events)[number]["type"];
// Type: "click" | "keydown"

Pattern 4: Function Arguments That Preserve Literal Types

A common frustration is passing a literal to a function and having TypeScript widen it. Const type parameters (TypeScript 5.0+) solve this at the function declaration site, but for older versions, as const at the call site is the workaround. Both approaches are worth knowing.

// TypeScript 5.0+: const type parameter preserves literals.
function createEvent<const T extends { type: string }>(event: T): T {
  return event;
}

// The return type is { type: "click"; x: 10 }, not { type: string; x: number }.
const clickEvent = createEvent({ type: "click", x: 10 });

// Pre-5.0 equivalent: caller uses as const.
function createEventLegacy<T extends { type: string }>(event: T): T {
  return event;
}

const legacyEvent = createEventLegacy({ type: "click", x: 10 } as const);

Gotchas and Pitfalls of as const

Mutability is gone. After you apply as const, every property is readonly. If you pass a const-asserted object to a function that expects a mutable type, TypeScript rejects it. The fix is to type the function parameter as Readonly<T> or use a readonly array type.

It works only on literals. You can't write someVariable as const where someVariable is already a computed value. The assertion must appear on an object literal, array literal, or primitive literal expression. Applying as const to a variable that was already typed as string doesn't do anything useful.

No runtime effect. This is purely a compile-time annotation. It doesn't call Object.freeze. If you need runtime immutability, you still need to freeze the object yourself.

const config = { port: 3000, host: "localhost" } as const;

// Compile error: cannot assign to 'port' because it is a read-only property.
// config.port = 4000;

// But at runtime, JavaScript does not enforce this.
// Use Object.freeze if you need runtime safety.
const frozenConfig = Object.freeze({ port: 3000, host: "localhost" });

// Passing to a function expecting mutable types will fail.
function updatePort(cfg: { port: number }) {
  cfg.port = 4000;
}
// Error: 'readonly' property 'port' is not assignable.
// updatePort(config);

as const vs Enum: When to Use Which

Prefer as const objects when you want zero runtime overhead, full tree-shaking, and the ability to derive union types from plain data. The only scenario where a TypeScript enum still wins is when you need reverse mapping from value to name at runtime (Direction[0] === "Up"), which only numeric enums support. For string-based constants (the vast majority of real-world cases) as const objects are strictly better. They produce cleaner JavaScript output, work naturally with keyof/typeof, and don't introduce a TypeScript-specific construct that confuses developers coming from JavaScript.

One more note: the satisfies operator (TypeScript 4.9+) pairs beautifully with as const. You can validate that your const object matches a certain shape without widening the inferred types, giving you both type safety and literal precision.

type ThemeShape = Record<string, { bg: string; fg: string }>;

// satisfies validates the shape; as const preserves the literals.
const theme = {
  light: { bg: "#fff", fg: "#000" },
  dark: { bg: "#000", fg: "#fff" },
} as const satisfies ThemeShape;

// You get both: validated shape AND literal types.
type ThemeName = keyof typeof theme;
// Type: "light" | "dark"

type LightBg = (typeof theme)["light"]["bg"];
// Type: "#fff" — not string.
← Back to all articles