---
title: 'Const Types and as const'
source: 'https://academia.sh/en/courses/typescript/const-types-and-as-const'
course: TypeScript
language: en
updated: '2026-08-17T18:09:54+00:00'
license: 'CC BY-SA 4.0'
---

# Const Types and as const

Single-value literal types, stopping widening with as const, deriving a union type from a fixed list, and how the satisfies operator differs from a type annotation.

For a discriminated union to work, its fields had to be a literal type. The type
inference lesson, however, set a rule in the opposite direction: an object field's
type widens on its own and the literal type is lost.

This lesson resolves that tension. First it clarifies what literal types are, then
covers the `as const` notation that stops widening, and finally the `satisfies`
operator that checks a type without narrowing it.

A terminology note: **literal type**, as meant here, is a type containing a single
value. It is a relative of the **constant** concept from the Programming Fundamentals
course — a name bound to the same value throughout the program — but it is not the
same thing; one concerns a value's type, the other the immutability of a binding.

## What a Literal Type Is

Every value has a type containing only itself. `"C"` is a string value, and `"C"` is
also a single-element type. Such types are called literal types, and they are defined
for strings, numbers, and booleans.

Literal types are not very useful on their own — the declaration `const x: "C" = "C"`
says nothing extra. Their value shows up in a union: type `"C" | "Pa" | "%"` is a
three-valued set that rejects a fourth value.

## Widening and `as const`

Declaring the same object literal in two forms lets the inferred types be compared:

```typescript
export const direct = { id: "s-01", unit: "C", value: 21.4 };
export const frozen = { id: "s-01", unit: "C", value: 21.4 } as const;
export const units = ["C", "Pa", "%"];
export const unitsFixed = ["C", "Pa", "%"] as const;
```

Saved under the name `const-types.ts` and compiled with
`tsc --strict --target es2022 --declaration --emitDeclarationOnly const-types.ts`,
the produced `const-types.d.ts` file is:

```typescript
export declare const direct: {
    id: string;
    unit: string;
    value: number;
};
export declare const frozen: {
    readonly id: "s-01";
    readonly unit: "C";
    readonly value: 21.4;
};
export declare const units: string[];
export declare const unitsFixed: readonly ["C", "Pa", "%"];
```

The `as const` notation does three things at once:

1. It **does not widen** the fields' types; each field keeps its own literal type.
2. It marks every field **readonly**.
3. It types an array literal as a **tuple**, not an array: `readonly ["C", "Pa", "%"]`
   instead of `string[]`.

The third point matters. The `units` array's type has forgotten the element count;
the `unitsFixed` tuple's type knows the count, the order, and the value at every
position.

The readonly marking gives a guarantee at the type level too:

```typescript
const units = ["C", "Pa", "%"] as const;
units.push("F");
```

```text
f2.ts(2,7): error TS2339: Property 'push' does not exist on type 'readonly ["C", "Pa", "%"]'.
```

## What `as const` Leaves at Runtime

The course's axis has to be tested again. When the following file is compiled with
`tsc --strict --target es2022 ac.ts`:

```typescript
const units = ["C", "Pa", "%"] as const;
const measurement = { id: "s-01", unit: "C" } as const;
console.log(units, measurement);
```

The JavaScript produced:

```javascript
"use strict";
const units = ["C", "Pa", "%"];
const measurement = { id: "s-01", unit: "C" };
console.log(units, measurement);
```

The output of `node ac.js` is `[ 'C', 'Pa', '%' ] { id: 's-01', unit: 'C' }`.

`as const` has been **entirely erased**. The array is an ordinary array at runtime;
the `push` call is accepted by JavaScript. Readonlyness is a valid statement in the
code the compiler sees — not a runtime protection. Real immutability requires runtime
tools like `Object.freeze`, and even that is shallow.

The same boundary also shows up when a value marked `as const` is assigned to a type
that is not readonly:

```typescript
type Unit = "C" | "Pa" | "%";
type Measurement = { id: string; value: number; unit: Unit };

const raw = { id: "s-01", value: 21.4, unit: "C" } as const;
const record: Measurement = raw;
console.log(record.unit);

record.value = 22.1;
raw.value = 22.1;
```

```text
f6.ts(9,5): error TS2540: Cannot assign to 'value' because it is a read-only property.
```

The eighth line does not error. The `record` declaration's type is `Measurement`, and
`Measurement` is not readonly; a non-readonly reference to the same object has been
obtained. Readonlyness is a property not of the object, but of the **reference**.

The fifth line being accepted shows the actual problem `as const` solves: without
`as const`, the `unit` field would widen to `string` and the assignment would be
rejected.

## Deriving a Type From a Fixed List

From a tuple declared with `as const`, a type can be derived that is the union of its
elements:

```typescript
const units = ["C", "Pa", "%"] as const;
type Unit = (typeof units)[number];

const valid: Unit = "Pa";
console.log(valid, units.length);

const invalid: Unit = "F";
```

```text
f1.ts(7,7): error TS2322: Type '"F"' is not assignable to type '"%" | "C" | "Pa"'.
```

There are two steps. The `typeof units` notation takes the type of a **value** — here,
`readonly ["C", "Pa", "%"]`. Then the `[number]` index gives the union of the types of
all the tuple's numerically indexed elements. The diagnostic's `"%" | "C" | "Pa"` text
is the result of this derivation.

This pattern's value is that it prevents the same information from being written
twice. The value list stays in a single place; it can be used both at runtime (walked
in a loop, used in validation) and at the type level. The "count all values" ability
an enum gives is obtained here without producing any runtime code.

## `satisfies`: Checking Without Narrowing

Writing a type on an object literal does two things at once: it checks the literal
**and** it fixes the declaration's type to that type. The second is not always
wanted:

```typescript
type Interval = { low: number; high: number };

const annotated: Record<string, Interval> = {
  temperature: { low: -40, high: 85 },
};

const checked = {
  temperature: { low: -40, high: 85 },
} satisfies Record<string, Interval>;

console.log(annotated.humidity.high);
console.log(checked.humidity.high);
```

```text
f4.ts(12,21): error TS2339: Property 'humidity' does not exist on type '{ temperature: { low: number; high: number; }; }'.
```

The eleventh line does not error. The `annotated` declaration's type is
`Record<string, Interval>`; this type says "an interval for every string key," so a
`humidity` key counts as valid too. No such key exists at runtime, and the program
crashes.

The `satisfies` operator **checks** that the literal fits the given type, but does not
change the declaration's type. The `checked` declaration's type is the narrow type
inferred from the literal; access to a nonexistent key is caught on the twelfth line.

The checking direction works too:

```typescript
type Interval = { low: number; high: number };

const intervals = {
  temperature: { low: -40, high: "85" },
} satisfies Record<string, Interval>;

console.log(intervals.temperature.high);
```

```text
f5.ts(4,28): error TS2322: Type 'string' is not assignable to type 'number'.
```

Comparison of the three notations:

| Notation | Checks the literal | Type of the declaration |
|---|---|---|
| `const x: T = {...}` | Yes | `T` |
| `const x = {...} as T` | No (forces it) | `T` |
| `const x = {...} satisfies T` | Yes | The narrow type inferred from the literal |

The middle row is the type assertion introduced in the first lesson, and it is a debt.
The bottom row preserves both the check and the detail inference gives.

## Summary

- A literal type is a type containing a single value; its usefulness shows up inside a
  union, and it is distinct from the "constant" concept, which is about the
  immutability of a binding.
- The `as const` notation stops widening, marks fields readonly, and types array
  literals as tuples.
- `as const` is entirely erased at runtime; readonlyness is a property of the
  reference, not the object, and does not provide real immutability.
- From a fixed tuple, the union of its elements is derived with `(typeof x)[number]`;
  the same information is kept in one place at both the value and type levels.
- The `satisfies` operator checks a literal against a given type but keeps the
  declaration's narrow type; a type annotation checks and widens, a type assertion
  only forces.

## Next Step

This topic built the types of individual values. Next comes naming and sharing these
types: how is the contract a module exposes written, how are different
implementations satisfying the same contract defined? The next topic starts with
interfaces and turns the measurement record's model into a reusable contract.
