Literal types
Pin a value's type to one exact string, number or boolean instead of the widened primitive.
type Alignment = "left" | "right" | "center"; // string literals
type Dice = 1 | 2 | 3 | 4 | 5 | 6; // numeric literals
type Truthy = true; // boolean literal
let x = expr as const; // const assertion blocks widening- Added in TypeScript 1.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$string literal | "value" | A type whose only inhabitant is that exact string. Written with the quoted text in type position. |
$numeric literal | 42 | A type whose only inhabitant is that exact number. Negative and floating-point literals are allowed. |
$boolean literal | true | false | A type whose only inhabitant is that exact boolean. `boolean` is itself the union `true | false`. |
$as constoptional | expr as const | A const assertion. Stops literal widening, makes object properties readonly and array literals readonly tuples. |
Errors
tsc reports "Type '\"x\"' is not assignable to type '\"left\" | \"right\"'" when a value outside the literal set is supplied, and "Cannot assign to 'k' because it is a read-only property" when writing to a const-asserted object.
Changelog
- PHP 2.0Numeric and boolean literal types joined string literal types, and discriminated-union narrowing on a literal property became reliable.
- PHP 3.4`as const` added. Before it, the only way to keep a literal type on an object property was an explicit annotation or an `as "GET"` assertion.
- PHP 4.3Template literal types can be combined with string literals, so `` `on${Capitalize<string>}` `` style patterns accept literal arguments.
A literal type has exactly one inhabitant. "left" is a type that accepts only the string "left"; 42 accepts only the number 42. On their own they are close to useless — a variable that can only ever hold one value is a constant. They earn their keep in unions: "left" | "right" | "center" gives you a closed set of options that autocompletes at the call site and rejects typos at compile time, without the runtime object an enum emits.
Widening is the thing that bites
TypeScript infers a literal type only where the value cannot change. const x = "left" is "left"; let x = "left" is string, because you are plainly going to reassign it. Object properties are mutable too, so { method: "GET" } infers method: string even under const — the binding is constant, the property is not.
The fixes, in order of preference: annotate the object with its type, add as const to the whole literal, or assert the single property with as "GET".
When to reach for it
Reach for literal types whenever a string or number is really an identifier from a fixed set: a discriminant tag, a route name, an HTTP method, a CSS keyword. Reach for as const whenever you build that set as a value and want the type derived from it rather than typed out twice.
Example
When to use it
- Constrain a function parameter to a fixed set of option strings instead of accepting any string.
- Tag the members of a discriminated union so `switch` narrows each branch.
- Derive a union of valid keys or paths from a single `as const` config object, so the list is written once.
Frequently asked questions
Why does my string become `string` instead of the literal I wrote?
Because the location is mutable, so TypeScript widens the literal. `let` bindings and object properties widen; `const` bindings of a primitive do not. Add `as const` to the literal expression, or annotate the target with the literal union, to keep the narrow type.
Should I use literal unions or an enum?
Prefer a literal union in most codebases. It has no runtime output, works with plain strings coming from JSON or an API, and narrows in `switch` just as well. Reach for an `enum` only when you specifically want a runtime object to iterate over or a numeric value mapping.
What is the difference between `as const` and `const`?
`const` freezes the binding; `as const` freezes the type. `const x = "a"` still lets an object property inside `x` widen, whereas `x as const` gives every literal in the expression its exact type, marks object properties `readonly` and turns array literals into `readonly` tuples.
More examples
Literal tags driving a discriminated union
Prints 12.566 then 9. The literal type on `kind` is what makes the `switch` narrow: inside `case "circle"` the compiler knows `radius` exists and `side` does not. The `never` assignment in `default` is an exhaustiveness check — add a `{ kind: "triangle" }` member and that line becomes a compile error pointing straight at the unhandled case.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
default: {
// If a third shape is added and not handled, this line stops compiling.
const exhaustive: never = s;
return exhaustive;
}
}
}
console.log(area({ kind: "circle", radius: 2 }).toFixed(3));
console.log(area({ kind: "square", side: 3 }));Deriving a union from an `as const` object
Prints /docs then blog, docs, home. Without `as const` the values would widen to `string` and `RoutePath` would collapse to `string`, losing every guarantee. With it, the route table is written once as a value and both the key union and the path union fall out of it, so adding a route to the object immediately updates both types.
const ROUTES = { home: "/", docs: "/docs", blog: "/blog" } as const;
type RouteName = keyof typeof ROUTES; // "home" | "docs" | "blog"
type RoutePath = (typeof ROUTES)[RouteName]; // "/" | "/docs" | "/blog"
function go(name: RouteName): RoutePath {
return ROUTES[name];
}
console.log(go("docs"));
console.log(Object.keys(ROUTES).sort().join(", "));
Discussion