Difficulty: Intermediate
Const assertions, written as `as const`, tell TypeScript to infer the most specific (narrowest) type possible for a value and make it deeply readonly. When you write `const colors = ["red", "green", "blue"] as const`, TypeScript infers the type as `readonly ["red", "green", "blue"]` instead of `string[]`. Every string becomes a literal type, the array becomes a readonly tuple, and all nested properties become readonly.
Without `as const`, TypeScript widens types to be practical for mutation. An object `{ name: "Alice", age: 30 }` is inferred as `{ name: string; age: number }`, and an array `[1, 2, 3]` as `number[]`. This is correct for mutable values, but when you have configuration objects, lookup tables, or constant data that should never change, `as const` gives you the precision of literal types throughout the entire structure.
Const assertions are particularly powerful when combined with the `typeof` operator to derive types from values. A common pattern is defining a constant array of allowed values and then deriving a union type from it: `const statuses = ["draft", "published", "archived"] as const; type Status = typeof statuses[number];` This gives you `"draft" | "published" | "archived"` - the type and the runtime values stay in sync automatically.
One important detail: `as const` creates deeply readonly types. All properties, nested objects, and array elements become readonly. This means you cannot modify any part of the structure after creation. If you need to pass a const-asserted value to a function that expects a mutable type, you may need to use a type assertion or adjust the function's parameter type to accept readonly values.
// Without as const: string[]
const colorsWide = ["red", "green", "blue"];
// With as const: readonly ["red", "green", "blue"]
const colorsNarrow = ["red", "green", "blue"] as const;
// Derive a union type from the const array
type Color = typeof colorsNarrow[number]; // "red" | "green" | "blue"
function isValidColor(value: string): value is Color {
return (colorsNarrow as readonly string[]).includes(value);
}
console.log(isValidColor("red"));
console.log(isValidColor("purple"));
console.log(colorsNarrow.length);
The `as const` assertion makes the array a readonly tuple of literal types. Using `typeof arr[number]` extracts a union of all element literal types - a very common and useful pattern.
// Without as const: { host: string; port: number; secure: boolean }
const configWide = { host: "localhost", port: 3000, secure: false };
// With as const: deeply readonly with literal types
const config = {
host: "localhost",
port: 3000,
secure: false
} as const;
// config.port = 8080; // Error: Cannot assign to 'port' because it is a read-only property
type Config = typeof config;
// { readonly host: "localhost"; readonly port: 3000; readonly secure: false }
console.log(`${config.host}:${config.port}`);
console.log(`Secure: ${config.secure}`);
With `as const`, every property becomes readonly and gets its literal type. The object is deeply immutable at the type level - any attempt to modify a property is a compile error.
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"
const PERMISSIONS = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"]
} as const;
type Permission = typeof PERMISSIONS[Role][number];
function getPermissions(role: Role): readonly string[] {
return PERMISSIONS[role];
}
console.log(getPermissions("admin").join(", "));
console.log(getPermissions("viewer").join(", "));
This pattern keeps your runtime values and types in sync. Add a new role to the ROLES array and the Role type updates automatically. This eliminates the possibility of types and values getting out of sync.
as const, Readonly inference, Deep immutability, Literal preservation