as const
Freezes a literal expression so its type stays exact and its arrays become readonly tuples.
expression as const
<const>expression // non-.tsx files only- Added in TypeScript 3.4
Parameters
| Parameter | Type | Description |
|---|---|---|
$expression | literal expression | An object literal, array literal, or primitive literal. Const assertions may only be applied immediately to simple literal expressions, not to computed ones. |
Return value
The const-asserted type of expressionThe expression typed as narrowly as possible: literal types are not widened, object literal properties become readonly, and array literals become readonly tuples.
Errors
Applying it to a computed expression is an error - "A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals".
Changelog
- PHP 3.4Introduced, with the three rules: no literal widening, readonly object properties, and readonly tuples from array literals.
- PHP 4.5const assertions are permitted on template literal expressions built from other constants.
- PHP 5.0const type parameters let a generic function apply the same narrowing at the call site, so callers no longer need to write as const on every argument.
By default TypeScript widens literals so variables stay reassignable: let x = "hello" is string, and [10, 20] is number[]. A const assertion switches that off. Three things happen at once — literal types are preserved, every property of an object literal becomes readonly, and array literals become readonly tuples with a fixed length.
When to reach for it
Its highest-value use is deriving types from values. Declare the list once and let the union follow: const ROUTES = [...] as const then type Route = (typeof ROUTES)[number]. You now have one source of truth that you can iterate at runtime and that the compiler also enforces — which is what most people actually want from an enum, without the emitted object.
It also fixes the classic discriminated-union failure where { kind: "circle" } widens kind to string and narrowing stops working.
What trips people up
The freeze is shallow in one specific way: it applies to the literal you wrote, not to values referenced from it. If a property holds a variable that already points at a mutable array, that array stays mutable — you can still push to it — even though the property itself is readonly.
And readonly is compile-time only. Nothing is frozen at runtime; use Object.freeze for that.
Example
When to use it
- Derive a union of route or status names from the array you already iterate
- Keep a discriminant literal so a switch can narrow the union
- Replace an enum with a plain object whose keys and values are both literal types
Frequently asked questions
How do I turn an array of strings into a union type?
Add as const to the array and index the type query with number: const ROUTES = ["a", "b"] as const; type Route = (typeof ROUTES)[number]. Without the assertion the array is string[] and the union collapses to string.
Why can't I apply as const to my expression?
Const assertions only attach to simple literal expressions - object literals, array literals, primitive literals and enum member references. (60 * 60 * 1000) as const is rejected; write the computed value out as a literal, or put the assertion on each operand instead.
Is as const the same as Object.freeze?
No. as const is compile-time only and disappears from the emitted JavaScript, so the object is still mutable at runtime through any untyped reference. Object.freeze actually prevents writes but does not give you literal types or readonly tuples.
More examples
The three rules, and the restriction
Each pair shows the default inference next to the const-asserted version. b2 is a fixed-length readonly tuple, so mutating methods are gone from its type entirely. The last block is the restriction: the assertion must sit directly on a literal, which is why the arithmetic expression is rejected but the pre-computed number and the per-branch assertions are accepted.
// 1. no literal widening
let a1 = "hello"; // string
let a2 = "hello" as const; // "hello"
// 2. array literals become readonly tuples
let b1 = [10, 20]; // number[]
let b2 = [10, 20] as const; // readonly [10, 20]
// b2.push(30); // error: push does not exist on a readonly tuple
// 3. object literal properties become readonly
let c1 = { text: "hello" }; // { text: string }
let c2 = { text: "hello" } as const; // { readonly text: "hello" }
// c2.text = "bye"; // error: cannot assign to a read-only property
// restriction: only simple literal expressions
// let d = (60 * 60 * 1000) as const; // error
let d = 3_600_000 as const; // fine
let e = Math.random() < 0.5 ? (0 as const) : (1 as const); // fine, 0 | 1Discriminated unions and the shallow-freeze trap
In getShapes the assertion keeps kind as the literals "circle" and "square", which is what lets the if/else narrow each element to the right shape and reach radius or sideLength. The second half is the trap: foo's own properties are readonly, but contents points at arr, an ordinary mutable array that the assertion never touched, so pushing to it is perfectly legal.
function getShapes() {
return [
{ kind: "circle", radius: 100 },
{ kind: "square", sideLength: 50 },
] as const;
}
for (const shape of getShapes()) {
if (shape.kind === "circle") {
console.log("Circle radius", shape.radius);
} else {
console.log("Square side length", shape.sideLength);
}
}
// Without as const, kind widens to string and neither branch narrows.
// The freeze does not reach through a reference:
let arr = [1, 2, 3, 4];
let foo = { name: "foo", contents: arr } as const;
// foo.name = "bar"; // error
// foo.contents = []; // error
foo.contents.push(5); // allowed - arr itself was never const-asserted
console.log(foo.contents.join(",")); // 1,2,3,4,5
Discussion