Suggest an editImprove this articleRefine the answer for “Const assertions (as const) in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`as const`** is a TypeScript const assertion that locks a value to its most specific literal types and marks all properties as `readonly` instead of widened primitives. ```typescript const config = { url: "https://api.example.com", port: 3000 } as const; // Type: { readonly url: "https://api.example.com"; readonly port: 3000 } ``` **Key:** compile-time only, zero runtime cost, deeply readonly with exact literal types.Shown above the full answer for quick recall.Answer (EN)Image**`as const`** is a TypeScript assertion that tells the compiler to infer the most specific literal types for a value, making every property deeply `readonly` instead of a widened primitive like `string` or `number`. ## Theory ### TL;DR - Without `as const`: TypeScript infers `string`, `number`, `string[]`. With it: `"exact value"`, `42`, `readonly ["a", "b"]`. - All properties become `readonly`. Arrays become fixed-length readonly tuples. Nesting is fully recursive. - Use it for static configs, enum-like constants, and React Query cache keys. Skip it for dynamic runtime data. - Compile-time only. Zero impact on emitted JavaScript. ### Quick example ```typescript // Without as const - widened types const api = { endpoint: "users", version: 1, tags: ["admin", "user"] }; // Type: { endpoint: string; version: number; tags: string[] } // With as const - literal types everywhere const api = { endpoint: "users", version: 1, tags: ["admin", "user"] } as const; // Type: { // readonly endpoint: "users"; // readonly version: 1; // readonly tags: readonly ["admin", "user"]; // } ``` That second object is now a precise contract. TypeScript blocks any reassignment, catches typos at the call site, and gives autocomplete for every value. ### Key difference TypeScript defaults to widened types so variables stay flexible during development. The literal `"users"` becomes `string` because you might reassign it later. `as const` says: never widen this. Every primitive locks to its exact value, and that transformation recurses through every nested object and array. ### When to use - Static config objects: `as const` gives autocomplete and prevents silent typos in property values. - Enum alternatives: derive union types from an object's values without maintaining two separate lists. - React Query keys: cache invalidation requires exact tuple types, not `string[]`. - Function return types: callers get `"dark"` instead of `string` when you need literal precision. - Skip it when values come from user input, API responses, or change at runtime. ### How the compiler handles this During the contextual typing phase, TypeScript's type checker traverses the AST and replaces primitive inferences with `LiteralTypeNode` entries. Arrays get wrapped in `ReadonlyArray`. Objects get readonly wrappers. All of this is erased during compilation: V8 sees a plain JavaScript object. The enforcement exists only inside TypeScript's static analysis pass. `as const` does not call `Object.freeze`. TypeScript will stop you from reassigning `config.port` at the type level, but JavaScript can still mutate the object at runtime. If you need real runtime safety, combine both. ### Common mistakes **Expecting runtime immutability** ```typescript const arr = [1, 2, 3] as const; arr.push(4); // TS Error: Property 'push' does not exist on type 'readonly [1, 2, 3]' // The underlying JS array is not frozen - runtime mutation is still possible ``` Fix: `const arr = Object.freeze([1, 2, 3] as const);` **Applying `as const` to a variable that is already widened** ```typescript const dynamic = "foo"; // already inferred as string, not "foo" const config = { key: dynamic } as const; // Type: { readonly key: string } - the literal is lost ``` `as const` cannot recover a literal that was widened earlier. Define the value inline or in its own `const`. **Thinking `as const` narrows function parameter types** ```typescript function log(config: { mode: string }) { /* ... */ } log({ mode: "debug" } as const); // Still passes `string` - the function signature is what TypeScript checks ``` To preserve literals in parameters, use generics: `function log<K extends string>(config: { mode: K })`. ### Real-world usage - React Query (TanStack): query keys as `readonly ["users"]` tuples for exact cache matching. - Zod: `z.enum(["admin", "editor"] as const)` to derive a union from an array without duplication. - Redux Toolkit: `status: "loading" as const` in action payloads to match discriminated union branches. - Express: route config objects with `method: "GET" as const` for type-safe route tables. ### Follow-up questions **Q:** What is the type of `{ a: 1, b: 2 } as const`? **A:** `{ readonly a: 1; readonly b: 2 }`. Both are literal number types, not `number`. **Q:** What is the difference between `as const` and `Readonly<T>`? **A:** `Readonly<T>` makes properties readonly but keeps widened types. `{ x: "a" }` wrapped in `Readonly` still gives `x: string`. `as const` does both: readonly and literal types. **Q:** Can you apply `as const` to a function return? **A:** Yes. `function getConfig() { return { port: 3000 } as const; }` - the caller sees `readonly port: 3000`, not `port: number`. **Q:** How deep does readonly go? **A:** Fully recursive. Every nested object and array becomes readonly until you reach primitive values. **Q:** How does `as const` differ from the `satisfies` operator (TypeScript 4.9+)? **A:** `satisfies` validates a value against an existing type while keeping literal inference. `as const` narrows to literals without checking against a specific type. You can combine them: `{ port: 3000 } as const satisfies ServerConfig`. **Q:** In RTK Query, why do cache tags need `as const`? **A:** Without it, `["Post", post.id]` widens to `string[]`. Tag matching requires exact tuple types. With `as const` you get `readonly ["Post", string]`, which the matcher can use correctly. ## Examples ### Deriving a union type from an object ```typescript const STATUS = { PENDING: "pending", ACTIVE: "active", INACTIVE: "inactive", } as const; type Status = typeof STATUS[keyof typeof STATUS]; // "pending" | "active" | "inactive" function updateUser(id: string, status: Status) { // only "pending" | "active" | "inactive" accepted } updateUser("123", STATUS.ACTIVE); // ✅ updateUser("123", "deleted"); // ❌ TS Error ``` The type updates automatically when you add or rename a value. No need to keep the union definition in sync manually. ### React Query cache keys ```typescript const QUERY_KEYS = { users: ["users"] as const, user: (id: string) => ["user", id] as const, } as const; function useUser(id: string) { return useQuery({ queryKey: QUERY_KEYS.user(id) }); } // Invalidation works because the types are exact tuples queryClient.invalidateQueries({ queryKey: QUERY_KEYS.users }); ``` I've seen this break in production: someone removes `as const` from the query keys file, cache stops invalidating, and it takes an hour to trace back. Exact tuples matter here. ### Nested objects and the runtime gap ```typescript const config = { db: { host: "localhost", port: 5432, flags: ["ssl", "compression"], }, } as const; // TypeScript blocks this: config.db.port = 5433; // Error: Cannot assign to 'port' because it is a read-only property // But plain JavaScript does not care: (config as any).db.port = 5433; // No runtime error ``` For config that genuinely must not change at runtime, add `Object.freeze`. TypeScript's readonly is a static guarantee. JavaScript's freeze is the runtime one. Different tools, different layers.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.