Typing with as const
In TypeScript, as const
allows you to lock literal values and make them as specific as possible: strings won't widen to string
, numbers won't widen to number
, and object properties and arrays become readonly
. This is useful when working with immutable objects or when using them as "enum-like" constructs. Docs: const
assertions
Let's see how it works in practice, and what pitfalls appear when comparing plain typing to as const
.
Examples
Explicit type annotation
type Fruit = { name: string };
const Apple: Fruit = { name: "Apple" }; // create an Apple object and annotate it as Fruit
Apple.name = "Orange"; // ⚠️ no error
The property remains mutable. A regular type annotation doesn't protect against changes.
Using as const
const Apple = { nme: "Apple" } as const; // the object is immutable
function isFruit(payload: unknown): payload is Fruit { return payload && "name" in payload; } // a simple type guard for clarity
isFruit(Apple); // ⚠️ false
Here the property name is misspelled (nme
instead of name
), so the check fails. This shows that as const
locks values but doesn't guarantee interface compatibility — typos or missing fields can slip through.
Combining as const
with an interface
It may seem natural to combine as const
with an interface:
const Apple: Fruit = { name: "Apple" } as const;
Apple.name = "Orange"; // ⚠️ no error
But in practice, as const
gets "is discarded": the type is narrowed to { name: string }
, so the property becomes mutable again.
Readonly
const Apple: Readonly<Fruit> = { name: "Apple" };
Apple.name = "Orange"; // ✅ compile-time error
This approach prevents property mutation, and the Fruit
type ensures field names are correct. But there's a catch: the IDE shows Apple's type as { name: string }
, not the literal { name: "Apple" }
. The "specific type" is lost — which is often the main reason for using as const
.
ReadonlyDeep
If Readonly
is enough for you, keep in mind it only works at the top level: nested objects and arrays stay mutable. In such cases, it's better to use ReadonlyDeep
from type-fest. It recursively makes all properties immutable.
The solution
To both lock values and validate structure, use the satisfies
operator. It checks that an expression is compatible with a given type while preserving the original (more specific) type for inference. Docs: satisfies operator
const Apple = { name: "Apple" } as const satisfies Fruit;
Apple.name = "Orange"; // ✅ compile-time error
With as const satisfies %type%
, TypeScript validates object fields correctly, as const
preserves literal immutability, and attempts to reassign a property are caught at compile time.
Conclusion
as const satisfies
is a straightforward way to achieve two goals at once: keep object values immutable and ensure the structure matches the interface. It's especially useful for large objects, such as configs, where typos or type mismatches are easy to make.