# Literal types

Source: https://codewiki.com/typescript/literal-types/

> - **what**: A literal type restricts a type to one exact value; unions of literals directly model protocol methods, states, and other closed choices.
> - **trap**: Literals in mutable positions usually widen to `string`, `number`, or `boolean`, while an assertion can hide that runtime input isn't a member of the union at all.
> - **fix**: Use annotations to declare contracts, `as const` to preserve constant data, `satisfies` to check shapes, and runtime validation before external data enters a union.

## What it is and why it exists

A literal type contains one exact value, such as the string `"queued"`, the number `404`, the bigint `10n`, or the Boolean value `true`.
`string` represents every string, while `"queued"` represents only that one string.
This precision lets the compiler decide whether a value really belongs to a finite protocol, rather than merely sharing its primitive type.

Production code usually combines several literals in a union type, such as `"GET" | "POST"`.
That type describes a closed set: a value may be `"GET"` or `"POST"`, but not any other string.
String literals are the most common form; number, `bigint`, and Boolean literals follow the same assignability rule.

Literal types answer "which exact values are allowed here," not just "which primitive kind is stored here."
That difference matters when a business rule has a few stable choices, such as job states, command names, log levels, retry counts, or response tags.
A misspelled state is rejected during compilation, and the editor can offer completions from the union.

Literal fields can also connect an object's tag to its payload.
If the member with `status: "complete"` always has a `url`, while `status: "failed"` always has a `message`, checking `status` makes the matching field safe to use.
This structure is a discriminated union.

A literal type isn't an input validator, and its information doesn't survive in the emitted JavaScript.
Network responses, JSON, environment variables, and ordinary JavaScript callers can still provide values outside the set.
Those values should enter the boundary as `unknown` or a broad runtime type and receive the literal union only after real checks pass.

If the allowed values are naturally open, such as usernames or arbitrary URLs, keep using `string`.
Turning every example value into a union misrepresents data as a closed protocol and makes routine expansion require pointless type edits.
Literal types fit finite, enumerable choices maintained by the program.

## How it works

The type checker treats a type as a set of possible values.
A literal type is a one-element set, and the union operator `|` combines sets.
Therefore, `type Speed = "standard" | "express"` contains exactly two strings, and a `Speed` is still assignable where any `string` is accepted.

The reverse assignment isn't safe.
An ordinary `string` might be `"overnight"` at runtime, so it can't be assigned to a parameter that accepts only `Speed`.
Assignability depends on whether every possible source value belongs to the target set, not whether the current runtime value happens to look right.

### Values, types, and assignability

The expression `"express"` produces a JavaScript string and gives the checker exact literal information.
The annotation `const speed: Speed = "express"` checks that the initializer belongs to the union and fixes the variable's public contract as `Speed`.
If the union changes later, every site that depends on that contract is checked again.

A literal union has no runtime container.
Unlike a regular `enum`, it emits no object to inspect, and you can't enumerate it with `Object.values(Speed)` because `Speed` exists only in a type position.
When you also need a runtime option list, derive the union from an `as const` tuple so the value and type share one source.

The Boolean type can be understood as `true | false`.
The individual type `true` can express a satisfied condition or a generic branch result, but an ordinary feature flag should usually remain `boolean`.
Number literals suit a few domain-specific codes or levels; they don't describe an arbitrary integer range.

`null` and `undefined` also have one runtime value each and often appear in unions.
They mainly model absence; this topic focuses on literals produced by string, number, `bigint`, and Boolean expressions that participate in widening.

### Inference and literal widening

Type inference gathers information from initializers, call arguments, and the surrounding expected type.
The checker has to balance precision against later mutation.
An immutable primitive `const` can stay exact, while a `let` usually needs to admit other values of the same primitive kind.

For example, `const mode = "dark"` usually has type `"dark"` because the variable binding can't change to another value.
`let mode = "dark"` usually has type `string`; otherwise, a valid next line such as `mode = "light"` would be blocked by the initializer.
The move from an exact literal to a broader primitive is literal widening.

`const` fixes only the variable binding; it doesn't make object properties readonly.
With `const request = { method: "GET" }`, the later assignment `request.method = "POST"` remains possible, so `method` is normally inferred as `string`.
Likewise, the ordinary array literal `const methods = ["GET", "POST"]` normally becomes `string[]`, not a fixed-length tuple.

An explicit context can prevent premature widening.
If a variable is declared as `const method: "GET" | "POST" = "GET"`, or an object is passed to a parameter that declares that union, its initializer is checked against the expected type.
The context supplies the allowed set, and the expression must belong to it.

This comparison summarizes the usual static result of each declaration.

| Form | Main result | Restricts runtime mutation? |
| --- | --- | --- |
| `const value = "on"` | The binding usually retains `"on"` | Only prevents rebinding the variable |
| `let value = "on"` | Usually widens to `string` | No |
| `const item = { mode: "on" }` | `mode` is usually `string` | No |
| `const item = { mode: "on" } as const` | `mode` is readonly `"on"` | Does not call `Object.freeze` |

### `as const` preserves literal information

A const assertion, written `as const`, asks the checker not to widen a literal expression.
Object-literal properties become `readonly`, and array literals become readonly tuples whose elements retain their own literal types.
It fits option tables, route tables, action definitions, and other constant data maintained in source.

`as const` is a static instruction, not a runtime freeze.
The type syntax doesn't appear in the emitted JavaScript, and existing mutable objects referenced by the literal aren't recursively copied or frozen.
If runtime callers might mutate an object, design its ownership separately or apply a real freezing operation.

`as const` can also produce a type narrower than the API's contract.
If an object really needs to move among several allowed values, give it a mutable union annotation rather than applying a const assertion and forcing casts later.
More precision isn't always better; the precision has to match the value's lifetime.

### `satisfies` checks without replacing the expression type

The `satisfies` operator checks that its left-hand expression is assignable to the right-hand type while retaining useful details from the expression's inferred type.
It works well for checking that a configuration covers required keys and that property values belong to a union, while keeping each written value specific.
Unlike a direct annotation, it generally doesn't replace the variable's whole visible type with the target type.

`satisfies` doesn't make an object readonly and doesn't validate runtime data.
Combine it with `as const` where a readonly literal is wanted; run value checks where JSON needs validation.
They solve different problems and aren't interchangeable merely because both syntaxes follow an expression.

### Comparisons trigger narrowing

When a union value is compared with a literal, control-flow narrowing removes impossible members in the matching branch.
For `Speed`, the condition `speed === "express"` makes the value exactly `"express"` in the true branch and leaves only `"standard"` in the false branch.
The result combines an ordinary JavaScript comparison with static union information.

A discriminated union extends this rule to a whole object.
Every member shares a property name but assigns it a different literal value; after that property is checked, TypeScript narrows the other fields too.
The tag must actually distinguish the members, rather than being the same broad `string` on every member.

A `switch` is a natural way to handle a closed union, but a new member is guaranteed to expose omissions only when you write an exhaustiveness check.
After every known branch returns, the remaining value should be `never`; passing it to a helper that accepts only `never` lets the compiler verify that fact.
A catch-all `default` that accepts anything hides new members instead.

## Examples

These four examples start with one union, establish a single source for values and types, use literals as object tags, and finally compare `satisfies` with a direct annotation.
Each program runs on its own without network access or supporting files.

### Restrict a function input

`ShippingSpeed` restricts shipping speed to two allowed values.
The comparison inside the function also narrows each branch to its matching literal.

<!-- quick -->

```typescript
// file: shipping.ts
type ShippingSpeed = "standard" | "express";

function dispatch(orderId: string, speed: ShippingSpeed): string {
  const transit = speed === "express" ? "1 day" : "4 days";
  return `${orderId}: ${speed} (${transit})`;
}

const selected: ShippingSpeed = "express";

console.log(dispatch("order-104", selected));
console.log(dispatch("order-105", "standard"));
```

```text
order-104: express (1 day)
order-105: standard (4 days)
```

<!-- /quick -->

The annotation makes the function boundary a closed contract, while callers still pass ordinary string literals directly.
Passing `"overnight"` reports an error at the call site instead of waiting for the function to take an unexpected branch.

The runtime output contains only ordinary strings.
`ShippingSpeed` is erased during compilation, so the precise type introduces no extra runtime object or conversion.

### Derive a union from a runtime tuple

When a program needs both runtime enumeration and a reusable static type, write the value list first.
`typeof CHANNELS[number]` reads the element types at every numeric index of the readonly tuple, producing `"email" | "sms" | "push"`.

```typescript
// file: channels.ts
const CHANNELS = ["email", "sms", "push"] as const;
type Channel = (typeof CHANNELS)[number];

function isChannel(value: string): value is Channel {
  return CHANNELS.some((channel) => channel === value);
}

function subscribe(input: string): string {
  if (!isChannel(input)) {
    return `unsupported: ${input}`;
  }

  return `subscribed: ${input}`;
}

console.log(CHANNELS.join(", "));
console.log(subscribe("push"));
console.log(subscribe("fax"));
```

```text
email, sms, push
subscribed: push
unsupported: fax
```

The tuple is a runtime fact, and `Channel` is the static fact derived from it.
Adding a channel in `CHANNELS` updates iteration, the guard, and the union type from one edit.

`isChannel` performs real comparisons, so it can narrow an external string to `Channel`.
The type predicate `value is Channel` describes the fact proved by a successful return; the checker won't detect a lie if the implementation disagrees with that predicate.

This pattern suits a short list owned by the current program.
If the server deployment, a plugin, or a database decides which values are allowed, a build-time tuple can't pretend to be the complete runtime source.

### Associate state with data through literal tags

Every `UploadState` member has `status`, but its other fields depend on that tag.
After the `switch` checks the tag, each branch can access only the payload that state really owns.

```typescript
// file: upload-state.ts
type UploadState =
  | { status: "queued"; file: string }
  | { status: "uploading"; file: string; percent: number }
  | { status: "complete"; file: string; url: string }
  | { status: "failed"; file: string; message: string };

function assertNever(value: never): never {
  throw new Error(`Unexpected state: ${JSON.stringify(value)}`);
}

function describe(state: UploadState): string {
  switch (state.status) {
    case "queued":
      return `${state.file}: waiting`;
    case "uploading":
      return `${state.file}: ${state.percent}%`;
    case "complete":
      return `${state.file}: ${state.url}`;
    case "failed":
      return `${state.file}: ${state.message}`;
    default:
      return assertNever(state);
  }
}

const states: UploadState[] = [
  { status: "queued", file: "avatar.png" },
  { status: "uploading", file: "report.pdf", percent: 60 },
  { status: "complete", file: "map.svg", url: "/files/map.svg" },
];

for (const state of states) console.log(describe(state));
```

```text
avatar.png: waiting
report.pdf: 60%
map.svg: /files/map.svg
```

The main purpose of `assertNever` is visible during type checking.
If you add a `"paused"` member to `UploadState` without a matching branch, `state` is no longer `never` in `default`, and compilation fails.

The `throw` in the helper still has runtime value.
Unchecked JavaScript or a bad assertion can bypass the static type, and an unknown runtime tag won't silently produce plausible text.

Don't replace this union with one interface containing a broad `status: string` and several optional fields.
That shape allows invalid combinations such as `status: "complete"` without `url`, and control flow can't establish relationships among the fields.

### Check a configuration shape with `satisfies`

The configuration must contain exactly two environments, and each retry count must belong to the declared union.
`satisfies` performs those checks while keeping the two written retry values as `0` and `3`, respectively.

```typescript
// file: service-config.ts
type Environment = "development" | "production";
type ServiceConfig = {
  endpoint: string;
  retry: 0 | 1 | 2 | 3;
};

const services = {
  development: { endpoint: "http://localhost:3000", retry: 0 },
  production: { endpoint: "https://api.example.com", retry: 3 },
} satisfies Record<Environment, ServiceConfig>;

function retryLabel(count: 0 | 3): string {
  return count === 0 ? "no retries" : "three retries";
}

console.log(`${services.development.endpoint}: ${retryLabel(services.development.retry)}`);
console.log(`${services.production.endpoint}: ${retryLabel(services.production.retry)}`);
```

```text
http://localhost:3000: no retries
https://api.example.com: three retries
```

A misspelled key or `retry: 5` reports an error on the configuration object.
If the variable were directly annotated as `Record<Environment, ServiceConfig>`, reading a specific property would expose the full retry union `0 | 1 | 2 | 3`.

This example doesn't use `as const`, so mutable properties such as `endpoint` haven't all become readonly.
The requirement is shape checking plus literal precision, not an assumption that the whole configuration can never change.

The right side of `satisfies` also disappears from emitted JavaScript.
If this configuration comes from a file rather than a source object, its contents still need parsing and validation.

## Pitfalls

> **Pitfall:** Assuming `const` preserves the literal type of object properties. `const request = { method: "GET" }` only prevents rebinding `request`; the property stays mutable, so `method` is normally `string`.

**Fix:** Give the property a union annotation if the object will change; use `as const` if it is constant data.
Use `satisfies` when you only need to check the shape without replacing the inferred type.

> **Pitfall:** Treating `as const` as a runtime freeze or input validator. It doesn't call `Object.freeze`, and it can't prove that a string received from JSON or the network belongs to a union.

**Fix:** Perform a membership check at the trust boundary, and decide separately whether the object's real ownership requires freezing.
Type precision, runtime validity, and mutability are three separate guarantees.

> **Pitfall:** Repairing a widening error with a type assertion, such as writing an arbitrary `string` as `value as Status`. An assertion asks the checker to trust the author; it neither changes nor inspects the runtime value.

**Fix:** For values controlled in source, correct the inference or annotation at the declaration; for external values, validate before returning the union.
Don't make an assertion the general conversion from a broad type to a narrow one.

> **Pitfall:** Adding the broad primitive to a literal union, as in `"auto" | string`. Because `string` already includes `"auto"`, the whole union accepts every string and loses its closed-set constraint.

**Fix:** Decide first whether the API is a closed set or an open string.
When arbitrary extension values are genuinely allowed, design runtime validation, editor completion, and compatibility policy for an open protocol; don't claim the union rejects unknown values.

> **Pitfall:** Returning a generic result from a `default` branch and assuming every union member is safely handled. When a new literal member appears, this branch swallows the omission and the compiler has no reason to complain.

**Fix:** End control flow in every known branch, then pass the remaining value to `assertNever`.
Keep the runtime throw as well, so unchecked call paths are exposed.

> **Pitfall:** Using number literal unions to represent arbitrary ranges, such as trying to list every valid port or monetary amount. A union can enumerate a few named levels, but it can't describe a continuous range or enforce runtime business rules.

**Fix:** Use `number` for open numeric input and check finiteness, integrality, and range at runtime.
Write `0 | 1 | 2 | 3` only when those numbers are themselves domain choices.

<!-- deep -->

## Four tools for controlling precision

Annotations, `as const`, `satisfies`, and ordinary assertions can all appear next to a value, but they answer different questions.
First decide whether the site declares a contract, preserves expression precision, checks a shape, or supplies evidence the compiler can't derive.

| Tool | What it asks the checker to do | Typical risk |
| --- | --- | --- |
| Type annotation | Give a variable or boundary the declared target type | May intentionally widen the initializer |
| `as const` | Preserve literals and view the literal structure as readonly | Mistaken for a runtime freeze |
| `satisfies` | Check assignability to a target while retaining useful inference | Mistaken for runtime validation |
| `as SomeType` | Accept type evidence supplied by the author | The evidence may be wrong |

Public function parameters and returns generally suit annotations because callers need a stable contract.
An options table in source suits `as const` because the value is the runtime source and shouldn't be changed casually.
A large configuration object suits `satisfies` because you often want to check its keys and values while retaining specific property information.

Keep ordinary assertions for small seams already proved by some other mechanism but beyond what the checker can express.
If the same assertion repeats, go back to the data source, function signature, or validator and repair the type flow.
Many occurrences of `as Status` usually mean the program hasn't recorded how a value becomes a `Status`, not that literal types are too strict.

These tools can be combined, and their order expresses a specific intent.
For example, a static lookup table can preserve readonly literals with `as const` and use `satisfies` to check that it covers a key set.
The combination still performs no runtime validation because both pieces of syntax are erased.

## `const` type parameters and call-site inference

TypeScript also allows `const` before a generic type parameter, which gives object, array, and primitive literals written directly at a call site a more `as const`-like inference preference.
This is useful for tuple factories, route definers, and event-table helpers because callers don't have to repeat `as const` after every argument.
It changes inference preference, not the function parameter's runtime value.

A `const` type parameter affects candidates in the call expression only.
If a value is first stored in an already widened variable and then passed to the generic function, its lost literal information isn't recovered.
The API author still needs a constraint compatible with the intended readonly result.

Don't mark every generic parameter `const` merely to preserve every local literal.
When callers need mutable collections or a broad return contract, overly narrow inference adds assignment friction.
It fits APIs where "the structure written at the call site is part of the contract."

## Static sets and the runtime world

A literal union is closed only along code paths checked by TypeScript.
After type erasure, a JavaScript function can still receive any string, while `any`, bad assertions, and unvalidated data can all bypass the set.
The system therefore needs an explicit step that promotes a runtime value to a validated union member.

A short, stable set can use one readonly tuple to drive validation and typing, as the `CHANNELS` example does.
Larger protocols often have a schema, interface description, or server contract; generate the runtime parser and TypeScript type from that authority, then test that they stay synchronized.
Two handwritten lists that look the same will drift when a member is added or removed.

Membership validation is only the first step.
External input for a discriminated union must also be a non-null object, have a string discriminant, and contain every payload field required by that member.
Checking only `status === "complete"` and asserting the whole object still admits data with no `url`.

Error handling is part of the protocol design too.
An internally impossible state can use `assertNever` to expose a programming error, while an unknown external value may need a structured parse error, a compatibility event, or a forward-compatible path.
Don't use one silent `default` for both cases.

The most reliable use of literal types connects the static contract to runtime evidence through one reviewable path.
Source constants supply options, a validator proves that input belongs to them, the union preserves that proof, and control flow selects the legal payload by literal.
If an assertion skips any step, the precise types downstream look more trustworthy than the real data is.

<!-- /deep -->

[Checkpoint: typescript/literal-types](https://codewiki.com/typescript/literal-types/#checkpoint)

## Further reading

- [TypeScript Handbook: literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)
- [TypeScript Handbook: discriminated unions and exhaustiveness checking](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)
- [TypeScript 3.4 release notes: `const` assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
- [TypeScript 4.9 release notes: the `satisfies` operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator)
- [TypeScript 5.0 release notes: `const` type parameters](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters)
