TypeScript's `as const`, Explained
TypeScript's as const assertion locks a value to its literal, readonly type instead of widening it. How it works and when to reach for it.
The as const assertion tells TypeScript to infer the narrowest, most literal type possible for a value instead of the general type it would normally widen to, and to treat arrays and objects as readonly. It’s a one-word way to stop the compiler from generalizing a value you meant to be exact.
The widening problem
By default, TypeScript widens literal values when it infers their type. Write let status = "active" and TypeScript infers string, not "active", because a let binding might be reassigned to any string later. This is usually the right call, but it causes friction in a common pattern: defining a fixed set of allowed values.
const config = {
method: "GET",
retries: 3,
};
// config.method inferred as `string`, config.retries as `number`
If a function expects method: "GET" | "POST", passing config.method fails — TypeScript sees a string, and a string isn’t assignable to a narrower union, even though the actual value is fine. The type has already been widened past the information you need.
What as const does
Appending as const to a value freezes its type at the literal level and makes any array or object properties readonly:
const config = {
method: "GET",
retries: 3,
} as const;
// config.method is the literal type "GET", not string
// config.retries is the literal type 3, not number
// config is deeply readonly
Now config.method has type "GET" — a literal, single-value type — which is assignable anywhere "GET" | "POST" is expected. The readonly side effect is also useful on its own: attempting config.retries = 5 is a compile error, catching accidental mutation of values meant to be constant.
Arrays behave the same way, with an added benefit: as const turns a mutable array into a fixed-length readonly tuple type, where each position keeps its own literal type instead of collapsing to a single element type:
const point = [10, 20]; // number[]
const fixedPoint = [10, 20] as const; // readonly [10, 20]
Deriving unions from arrays
This matters most in a common pattern: combining as const with the typeof operator to define a set of allowed values once and derive a union type from it, instead of maintaining the list and the type separately:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
ROLES is both a runtime value — you can .map() or .includes() it — and the source of a compile-time union type. Change the array and the type updates automatically; there’s no second list to keep in sync. This is a cleaner alternative to declaring an enum purely to enumerate string values, since it produces plain string literals at runtime with no extra generated code.
as const vs satisfies
as const and the satisfies operator solve adjacent but different problems, and they compose well together. as const narrows a value’s inferred type to its literal form. satisfies checks that a value matches a given type without changing its inferred type. Using both together is common: satisfies validates the shape against an interface, and as const keeps the literal types intact.
type Endpoint = { method: "GET" | "POST"; path: string };
const endpoint = {
method: "GET",
path: "/users",
} as const satisfies Endpoint;
Here TypeScript checks the object is a valid Endpoint (catching typos like "Get"), while endpoint.method still has the precise literal type "GET" rather than widening to "GET" | "POST".
| Effect | Typical use | |
|---|---|---|
| No assertion | Widens to general type | Mutable values |
as const | Narrows to literal, adds readonly | Fixed configs, deriving unions |
satisfies | Validates against a type, keeps inference | Checking shape without losing literals |
Where it doesn’t help
as const only affects what TypeScript infers — it’s erased at compile time and has zero runtime effect, same as every other type assertion. It doesn’t validate data coming from outside your code (an API response, JSON.parse output, user input); those still need actual runtime checks. It’s also not a substitute for generics when you need a function’s return type to vary based on its input’s literal type — that requires generic type parameters, not a value-level assertion.
The takeaway
as const stops TypeScript from widening a literal value’s type and makes its structure readonly, which is exactly what you want for fixed configuration objects, enum-like string sets, and tuples. Combine it with typeof value[number] to derive a union type from an array without maintaining two sources of truth, and pair it with satisfies when you also need to check the value’s shape against an interface. It has no runtime behavior of its own — it’s purely a signal to the compiler about how narrow to make its inference.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.