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.
// 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 propertyas 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.
// 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); // 3Extracting 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.
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"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.
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'); // Errortypeof 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.
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 RoutePathas 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.
// 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.
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 |
|
Extendable / mergeable | No | Yes (spread: |
Accepts raw string literals | No (string enum) | Yes — |
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.
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)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
Forgetting that
as constis type-only — it does not freeze the object at runtime. UseObject.freezeif you need runtime immutability.Using
as conston a mutable ref (like a ReactuseRefvalue) expecting it to prevent reassignment — refs are mutable by design.Putting
as conston a variable that is later reassigned — the assertion applies to the value expression, not the variable binding.Expecting
as constto validate the shape of an object — it only narrows the types; usesatisfiesfor validation.Forgetting to use
as constwhen passing an object literal to a function that expects literal types — the literal is widened before it reaches the function.
// 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 constfreezes literal types and makes all propertiesreadonlyOn arrays: produces a
readonlytuple of literal typesOn objects: deeply marks all nested properties as
readonlywith literal typestypeof arr[number]extracts a union from aconstarraytypeof obj[keyof typeof obj]extracts a union of values from aconstobjectDoes NOT call
Object.freeze— runtime mutation is still possible in JSUse
satisfiesalongsideas constfor shape validation without type wideningPreferred over enums for application constants in modern TypeScript codebases