TypeScriptconst Assertions (as const)

const Assertions

The as const assertion tells TypeScript: "treat this value as the most specific type possible and make everything readonly". It is one of the most useful single features in TypeScript for eliminating unnecessary type widening without resorting to full type annotations.

What as const Does

Without as const, TypeScript widens literal values to their general types. With as const, every value is frozen at its exact literal type and every property becomes readonly.

TS
// Without as const — widened types
const config = {
  host: 'localhost',
  port: 3000,
  ssl:  false,
};
// type: { host: string; port: number; ssl: boolean }

// With as const — literal types, all readonly
const config2 = {
  host: 'localhost',
  port: 3000,
  ssl:  false,
} as const;
// type: { readonly host: "localhost"; readonly port: 3000; readonly ssl: false }

// Attempting to mutate is now a compile-time error
config2.host = 'production'; // Error: Cannot assign to 'host' because it is a read-only property
Note
as const is a type-level assertion — it does not call Object.freeze at runtime. The object is still technically mutable in JavaScript; only TypeScript will prevent you from mutating it.
as const on Arrays

Applied to an array, as const makes the array readonly and infers a tuple type with exact literal values instead of a general array type.

TS
// Without as const — widened array
const colors = ['red', 'green', 'blue'];
// type: string[]

// With as const — readonly tuple of literals
const colors2 = ['red', 'green', 'blue'] as const;
// type: readonly ["red", "green", "blue"]

// You cannot push, pop, or reassign elements
colors2.push('yellow'); // Error: Property 'push' does not exist on type 'readonly [...]'
colors2[0] = 'pink';    // Error: Cannot assign to '0' because it is a read-only property

// But you can still read them
console.log(colors2[0]); // "red"
console.log(colors2.length); // 3
Extracting Union Types from as const Arrays

One of the most powerful patterns: derive a string union type automatically from an as const array. This keeps the source of truth in one place — the array — rather than duplicating it in a type.

TS
const ROLES = ['admin', 'member', 'guest'] as const;

// Extract the union from the array elements
type Role = typeof ROLES[number];
// => "admin" | "member" | "guest"

function hasRole(role: Role) {
  return ROLES.includes(role);
}

hasRole('admin');   // ✓
hasRole('hacker');  // Error: Argument of type '"hacker"' is not assignable to type 'Role'

// The array and type stay in sync automatically
// Adding 'moderator' to ROLES updates the Role type too
const ROLES2 = ['admin', 'member', 'guest', 'moderator'] as const;
type Role2 = typeof ROLES2[number];
// => "admin" | "member" | "guest" | "moderator"
Success
typeof arr[number] is the idiomatic way to derive a union type from a const array. It is a zero-maintenance pattern — add a value to the array and the type updates automatically.
as const on Objects

For objects, as const deeply freezes all nested properties to their literal types and marks everything readonly recursively.

TS
const HTTP_METHODS = {
  Get:    'GET',
  Post:   'POST',
  Put:    'PUT',
  Patch:  'PATCH',
  Delete: 'DELETE',
} as const;

type HttpMethod = typeof HTTP_METHODS[keyof typeof HTTP_METHODS];
// => "GET" | "POST" | "PUT" | "PATCH" | "DELETE"

function request(method: HttpMethod, url: string) {
  return fetch(url, { method });
}

request(HTTP_METHODS.Get, '/api/users'); // ✓
request('YEET', '/api/users');           // Error
Tip
The pattern typeof OBJ[keyof typeof OBJ] extracts a union of all value types from a const object. This is the as const equivalent of deriving types from an enum.
Practical Example: Route Config

A route configuration object is a perfect use case — it is written once, referenced everywhere, and should never be mutated at runtime.

TS
const ROUTES = {
  home:      '/',
  dashboard: '/dashboard',
  profile:   '/profile',
  settings:  '/settings',
  login:     '/login',
  logout:    '/logout',
} as const;

type RoutePath = typeof ROUTES[keyof typeof ROUTES];
// => "/" | "/dashboard" | "/profile" | "/settings" | "/login" | "/logout"

type RouteName = keyof typeof ROUTES;
// => "home" | "dashboard" | "profile" | "settings" | "login" | "logout"

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

navigate(ROUTES.dashboard); // ✓
navigate('/random');         // Error: "/random" is not a valid RoutePath
as const on Function Return Values

When a function returns a value that should preserve its literal types, you can apply as const to the return. Without it, the return type is widened to general types.

TS
// Without as const — widened return type
function getTheme() {
  return { primary: '#0070f3', secondary: '#ff0080' };
}
// return type: { primary: string; secondary: string }

// With as const — literal return type
function getTheme2() {
  return { primary: '#0070f3', secondary: '#ff0080' } as const;
}
// return type: { readonly primary: "#0070f3"; readonly secondary: "#ff0080" }

const theme = getTheme2();
theme.primary; // type: "#0070f3" (literal, not just string)
Allowed Values List

A common real-world pattern: define an allowed values list as a const array and use it both as a runtime validator and a compile-time type.

TS
const ALLOWED_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CAD'] as const;
type Currency = typeof ALLOWED_CURRENCIES[number];

function formatCurrency(amount: number, currency: Currency): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

// Runtime validation using the same array
function isCurrency(value: string): value is Currency {
  return (ALLOWED_CURRENCIES as readonly string[]).includes(value);
}

const userInput = 'EUR';
if (isCurrency(userInput)) {
  formatCurrency(99.99, userInput); // TypeScript knows userInput is Currency here
}
as const vs Enums

The as const object pattern is often recommended over enums for application-level constants. Here is how they compare.

Feature

enum

as const object

Runtime representation

Generated enum object (extra boilerplate)

Plain JavaScript object

Tree-shakeable

No (enum object always emitted)

Yes

isolatedModules

const enum breaks it

Always works

String values

Only for string enums

Always — literals preserved

Derive union type

Not needed — the enum IS the type

typeof OBJ[keyof typeof OBJ]

Extendable / mergeable

No

Yes (spread: { ...OBJ, newKey: "val" } as const)

Accepts raw string literals

No (string enum)

Yes — "USD" works as well as CURRENCIES.USD

The satisfies Operator (TypeScript 4.9)

TypeScript 4.9 introduced the satisfies operator as a complement to as const. It validates that a value conforms to a type without widening the inferred type to that type.

as const preserves literals but does not validate the shape. satisfies validates the shape but does not widen. You can use both together.

TS
type Color = 'red' | 'green' | 'blue';
type Palette = Record<string, Color | { light: Color; dark: Color }>;

// Without satisfies — no validation, but literals preserved
const palette1 = {
  primary: { light: 'red',  dark: 'blue' },
  accent:  'green',
} as const;

// With satisfies — validated against Palette, literals still preserved
const palette2 = {
  primary: { light: 'red',  dark: 'blue' },
  accent:  'green',
} satisfies Palette;

palette2.accent.toUpperCase(); // ✓ — TypeScript knows this is string (not just Color)
// With a plain type annotation, it would be widened to Color

// Combine both for maximum strictness
const palette3 = {
  primary: { light: 'red',  dark: 'blue' },
  accent:  'green',
} as const satisfies Palette;

palette3.primary.light; // type: "red" (literal preserved AND shape validated)
Note
as const satisfies Type is the recommended modern pattern when you want both literal type preservation and shape validation. The order matters: as const comes first, satisfies second.
Common Mistakes
  1. Forgetting that as const is type-only — it does not freeze the object at runtime. Use Object.freeze if you need runtime immutability.

  2. Using as const on a mutable ref (like a React useRef value) expecting it to prevent reassignment — refs are mutable by design.

  3. Putting as const on a variable that is later reassigned — the assertion applies to the value expression, not the variable binding.

  4. Expecting as const to validate the shape of an object — it only narrows the types; use satisfies for validation.

  5. Forgetting to use as const when passing an object literal to a function that expects literal types — the literal is widened before it reaches the function.

TS
// Mistake: as const on a let variable does not prevent reassignment
let x = 'hello' as const; // type: "hello"
x = 'world'; // Error: Type '"world"' is not assignable to type '"hello"' ✓ (caught!)

// Mistake: forgetting as const on inline literals passed to functions
type Dir = 'left' | 'right';
function go(dir: Dir) {}

const opts = { direction: 'left' };   // type: { direction: string }
go(opts.direction);                    // Error: string not assignable to Dir

const opts2 = { direction: 'left' } as const;
go(opts2.direction);                   // ✓: type is "left"
Quick Reference
  • as const freezes literal types and makes all properties readonly

  • On arrays: produces a readonly tuple of literal types

  • On objects: deeply marks all nested properties as readonly with literal types

  • typeof arr[number] extracts a union from a const array

  • typeof obj[keyof typeof obj] extracts a union of values from a const object

  • Does NOT call Object.freeze — runtime mutation is still possible in JS

  • Use satisfies alongside as const for shape validation without type widening

  • Preferred over enums for application constants in modern TypeScript codebases