Alright, let’s talk about const assertions. You’ve probably used const for variable declarations to stop yourself from reassigning a primitive value. It’s like putting a child lock on a single cabinet.

const myName = "Dave"; // type is "Dave", not string
const answer = 42; // type is 42, not number

But what about when you want to do that for an entire object or array? You might think, “I’ll just declare it with const,” but you’d be wrong, and I don’t blame you. The const keyword in a variable declaration prevents reassignment of the variable itself, but it does nothing to make the contents of an object or array immutable. The type system still widens those values to their general types.

const myConfig = {
  host: "localhost",
  port: 8080,
  retry: true,
};
// Type is { host: string; port: number; retry: boolean; }
// I can still do this:
myConfig.port = 3000; // No error! The object is mutable.

This is where as const enters the stage, waving a tiny flag that says “I’M SERIOUS THIS TIME.” It’s a const assertion. You’re not casting a value; you’re telling TypeScript to infer the most specific, literal, and read-only types possible for every property in that expression.

What as const Actually Does

When you append as const to an expression, three things happen simultaneously:

  1. Literal Types are Locked In: No more widening to string or number. The literal values become the types.
  2. Properties Become readonly: Every property on the object is marked with the readonly modifier.
  3. Arrays Become Readonly Tuples: Arrays lose their mutable Array type and become read-only tuples, with their literal values preserved.

Let’s see the magic (or rather, the very precise engineering) happen:

const myConfig = {
  host: "localhost",
  port: 8080,
  retry: true,
  endpoints: ["/api", "/health"],
} as const;

Now, the inferred type for myConfig is:

{
    readonly host: "localhost";
    readonly port: 8080;
    readonly retry: true;
    readonly endpoints: readonly ["/api", "/health"];
}

Try to mutate anything now. I dare you.

myConfig.port = 3000; // Error: Cannot assign to 'port' because it is a read-only property.
myConfig.endpoints.push("/admin"); // Error: Property 'push' does not exist on type 'readonly ["/api", "/health"]'.

It’s effectively a deep-freeze at the type level. The runtime value is still a plain JavaScript object—you could try to mutate it and potentially get a runtime error in strict mode or just silently fail—but TypeScript will now protect you from all those shenanigans.

The Readonly Tuple Trade-Off

The array-to-tuple conversion is a massive win for type safety but can be a bit of a gotcha. Look at this:

const numbers = [1, 2, 3]; // number[]
const numbersAsConst = [1, 2, 3] as const; // readonly [1, 2, 3]

The type of numbersAsConst is not readonly number[]. It’s a tuple of the literal values 1, 2, and 3. This is incredibly precise, but it means methods like map and forEach will work, while mutators like push, pop, and sort won’t. This is exactly what you want for a configuration array or a list of fixed arguments.

Common Pitfalls and When to Use It

The most common pitfall is trying to use as const on a thing that can’t be more specific. as const on a variable that’s already widened is pointless and will rightfully cause an error.

let host = "localhost";
const config = { host } as const; // Error! 'host' is a string, not "localhost".

You use as const on the value itself, at the point of creation, to lock in its literalness. It’s perfect for:

  • Application Config Objects: Global settings that should never change.
  • Redux/Actions: Action creators and state slices benefit immensely from this level of immutability.
  • API Response Shapes (when you control the mock): For defining test fixtures with exact types.
  • Hard-coded Arrays/Lists: Instead of writing out a full enum or type for a simple set of values.

The One Big Limitation

Here’s the honest part: as const is a type-level assertion. It doesn’t recursively freeze your object at runtime. If you need true runtime immutability, you still need Object.freeze() or a library like Immer. However, using as const with Object.freeze() is a powerful combination—you get runtime and compile-time safety.

const myConfig = Object.freeze({
  host: "localhost",
  port: 8080,
} as const);
// Now it's truly locked down, everywhere.

Think of as const as your meticulous, hyper-vigilant code reviewer who points out every possible mutation you might do and tells you “nope” before you even run the code. It’s one of the simplest ways to make your TypeScript code radically more robust.