What Does 'as const' Do in TypeScript and When Should You Use It?
TypeScript's as const is a type assertion that tells the compiler to infer the narrowest possible type for a value. Instead of inferring string for a string literal or number[] for an array, TypeScript infers the exact literal value and marks everything as readonly. This single feature unlocks literal types, tuple inference, and object-as-enum patterns that would otherwise require verbose manual type annotations.
// Without as const: TypeScript widens the type.
const status = "active";
// type: "active" (already narrow because of const)
let status2 = "active";
// type: string (widened because let allows reassignment)
// as const forces the narrow literal type even in wider contexts.
const config = {
endpoint: "https://api.example.com",
retries: 3,
};
// type: { endpoint: string; retries: number }
const configNarrow = {
endpoint: "https://api.example.com",
retries: 3,
} as const;
// type: { readonly endpoint: "https://api.example.com"; readonly retries: 3 }
The Core Mechanic: Type Widening vs. Narrowing
TypeScript performs type widening by default on object properties and array elements. Even if you declare an object with const, the keyword only prevents reassignment of the binding. The properties remain mutable, so TypeScript widens "active" to string. Adding as const does three things at once: it narrows every value to its literal type, marks every property as readonly, and infers arrays as readonly tuples instead of mutable arrays.
// Arrays become readonly tuples.
const roles = ["admin", "editor", "viewer"];
// type: string[]
const rolesConst = ["admin", "editor", "viewer"] as const;
// type: readonly ["admin", "editor", "viewer"]
// This means you can extract a union type from the tuple.
type Role = (typeof rolesConst)[number];
// type: "admin" | "editor" | "viewer"
Replacing Enums with as const Objects
This is the most common real-world use case. TypeScript enums have quirks: they emit JavaScript code, numeric enums allow reverse-mapping bugs, and const enum has been flagged as problematic with certain bundlers. An as const object gives you the same developer experience with zero runtime overhead and full tree-shakeability.
const HttpStatus = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_ERROR: 500,
} as const;
// Derive the union type from the object values.
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];
// type: 200 | 404 | 500
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.NOT_FOUND) {
console.log("Resource missing");
}
}
// Compile error: 999 is not assignable to 200 | 404 | 500.
// handleResponse(999);
Fixing Literal Type Loss in Function Parameters
Without as const, passing an object or array to a function loses its literal types before the function ever sees them. This is a common source of confusion when you work with APIs that expect discriminated unions or specific string literals.
function createRequest(method: "GET" | "POST", url: string) {
return { method, url };
}
// This fails without as const.
const options = { method: "GET", url: "/users" };
// options.method is string, not "GET".
// createRequest(options.method, options.url); // Error!
// Fix: use as const.
const optionsFixed = { method: "GET", url: "/users" } as const;
createRequest(optionsFixed.method, optionsFixed.url);
Building Type-Safe Event Maps and Route Definitions
The pattern of extracting types from as const objects scales well for event systems, route definitions, and configuration registries. You define the data once and derive all types from it, creating a single source of truth.
const ROUTES = {
home: "/",
profile: "/profile/:id",
settings: "/settings",
} as const;
type RouteName = keyof typeof ROUTES;
// type: "home" | "profile" | "settings"
type RoutePath = (typeof ROUTES)[RouteName];
// type: "/" | "/profile/:id" | "/settings"
function navigate(route: RouteName) {
const path = ROUTES[route];
console.log(`Navigating to ${path}`);
}
// Compile error: "dashboard" is not a valid route.
// navigate("dashboard");
Gotcha: as const Makes Everything Deeply Readonly
The readonly modifier that as const applies is deep. Nested objects and arrays are all frozen at the type level. This is usually what you want for configuration and constants, but it can cause assignability issues when you pass values to functions that expect mutable types. If a library's type signature accepts string[], you can't pass a readonly string[] without a workaround.
const tags = ["ts", "js"] as const;
// type: readonly ["ts", "js"]
function processTags(input: string[]) {
input.push("new");
}
// Error: readonly ["ts", "js"] is not assignable to string[].
// processTags(tags);
// Workaround 1: spread into a mutable copy.
processTags([...tags]);
// Workaround 2: accept readonly in the function signature.
function processTagsSafe(input: readonly string[]) {
// input.push("new"); // Error — correctly prevented.
console.log(input.join(", "));
}
as const vs. Enums vs. Union Types: When to Pick Each
Use as const objects when you want a runtime-accessible lookup (for example, mapping status codes to messages) combined with a derived union type. Use a plain union type (type Dir = "north" | "south") when you don't need a runtime object at all. Use a TypeScript enum only when you're in a legacy codebase that already uses them or when you specifically need the reverse mapping of numeric enums. In greenfield projects, as const is almost always the better choice because it's standard JavaScript with zero magic.
One final tip: since TypeScript 5.0, you can write const type parameters on generic functions to get caller-side as const inference automatically. This eliminates the need for callers to remember to add as const themselves.
// TypeScript 5.0+ const type parameter.
function defineRoutes>(routes: T): T {
return routes;
}
// Caller gets literal types without writing as const.
const appRoutes = defineRoutes({
home: "/",
about: "/about",
});
// type: { readonly home: "/"; readonly about: "/about" }
type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
// type: "/" | "/about"