A literal type is a TypeScript type that allows only one value, such as the string “small”, the number 6 or the boolean true. On its own, a type with one value is rarely useful, but combined with the | operator, literal types form a closed list of allowed values. For example, a variable of type “small” | “medium” | “large” accepts those three strings and rejects “tiny” at compile time.
In TypeScript, we use literal types in many of the places where Java code uses an enum or a set of string constants, such as the status of an order in a shop app or the size options of a product.
The following example shows the six patterns that most real code with literal types uses, with the result of each line as a comment. The snippets compile with TypeScript 7.0.2 and run on Node.js 22.
// 1. A union of string literals
type Size = "small" | "medium" | "large";
let size: Size = "small";
size = "large"; // OK, "tiny" is a compile error
// 2. Number and boolean literals
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
const roll: Dice = 4;
const done: true = true;
// 3. const keeps the literal, let widens it
const fruit = "apple"; // fruit: "apple"
let other = "apple"; // other: string
// 4. Derive a union from an array
const sizes = ["small", "medium", "large"] as const;
type SizeFromArray = (typeof sizes)[number]; // "small" | "medium" | "large"
// 5. Check a runtime string
function isSize(value: string): value is Size {
return (sizes as readonly string[]).includes(value);
}
const valid = isSize("medium"); // valid = true
const invalid = isSize("tiny"); // invalid = false
// 6. One entry for every literal
const prices: Record<Size, number> = { small: 5, medium: 7, large: 9 };
const price = prices[size]; // price = 9
Notice that the const variable fruit keeps the literal type “apple”, whereas the let variable other gets the type string.
Next, we look at string, number and boolean literal types and at when the compiler widens a literal to string or number. After that come as const, deriving a union from an array, checking user input against a literal union, and Record objects with one entry per literal.
1. String, Number and Boolean Literal Types
We can use any string, number or boolean value as a type. We give the union a name with a type alias, so we write the list of values once and reuse it for variables, parameters, return types and object properties. For example, a coffee ordering app types the cup size of every order with its Size alias, so an order with the size “tiny” never compiles.
// 1. String literals
type Size = "small" | "medium" | "large";
const size: Size = "medium";
// 2. Number literals
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
const roll: Dice = 6;
// 3. Boolean literal
type Yes = true;
const agreed: Yes = true;
// 4. Literal type as a parameter
function order(fruit: "apple" | "banana", count: number): string {
return count + " x " + fruit;
}
const text = order("apple", 5); // text = "5 x apple"
String literal unions are the most common kind, whereas number literals fit small fixed sets such as dice values or HTTP status codes. The boolean type itself is defined as the union true | false, so a single boolean literal mostly appears inside object types, for example { success: true; value: number }.
A value outside the list fails to compile, both in an assignment and in a function call, and the editor offers the allowed values as autocomplete suggestions.
type Size = "small" | "medium" | "large";
let size: Size = "small";
size = "tiny";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
const roll: Dice = 7;
function order(fruit: "apple" | "banana", count: number): string {
return count + " x " + fruit;
}
order("cherry", 2);
src/err.ts(3,1): error TS2322: Type '"tiny"' is not assignable to type 'Size'.
src/err.ts(6,7): error TS2322: Type '7' is not assignable to type 'Dice'.
src/err.ts(11,7): error TS2345: Argument of type '"cherry"' is not assignable to parameter of type '"apple" | "banana"'.
TypeScript also has template literal types, which describe strings that follow a pattern, for example any string that ends in “px”. Helper types such as Uppercase change the case of a string literal type.
2. When TypeScript Infers a Literal Type
The compiler decides between a literal type and a general type based on whether the value can change. A const variable can never be reassigned, so it keeps the literal type. A let variable and an object property can change, so the compiler widens the literal to string, number or boolean, a process called widening.
Widening causes a common error. An object created with { size: “small” } has a size of type string, and we cannot pass a string where a Size is expected. The error shows up in a product form that builds its default values as an object literal and passes them to a function that expects Size. We have three ways to keep the literal type.
type Size = "small" | "medium" | "large";
function pick(size: Size): string {
return "Picked " + size;
}
// 1. Variables
const a = "small"; // a: "small"
let b = "small"; // b: string
// 2. Object properties widen too
const loose = { size: "small" }; // size: string
// 3. Three ways to keep the literal
const typed: { size: Size } = { size: "small" };
const checked = { size: "small" } satisfies { size: Size };
const frozen = { size: "small" } as const; // readonly size: "small"
const r1 = pick(a); // r1 = "Picked small"
const r2 = pick(frozen.size); // r2 = "Picked small"
Passing b or loose.size to pick() fails with the same message, because both values have the widened type string.
type Size = "small" | "medium" | "large";
function pick(size: Size): string {
return "Picked " + size;
}
let b = "small";
const loose = { size: "small" };
pick(b);
pick(loose.size);
src/err.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Size'.
src/err.ts(8,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Size'.
The three fixes differ in what they keep.
- A type annotation gives the property the type Size, so we can later assign “large” to it.
- The satisfies operator checks the object against the type and keeps the property as Size as well.
- The as const assertion makes the property read-only with the exact type “small”.
For configuration objects that never change, as const is the usual choice.
3. Building a Union From an Array With as const
A literal union written as a type exists only at compile time, so when the program also needs the list of values at runtime, for a dropdown or a validation check, we would have to write the values twice. The as const assertion avoids the second list, because we write the array once and derive the type from it.
// 1. From an array
const sizes = ["small", "medium", "large"] as const;
type Size = (typeof sizes)[number]; // "small" | "medium" | "large"
// 2. From object keys
const stock = { apple: 5, banana: 3 } as const;
type Fruit = keyof typeof stock; // "apple" | "banana"
// 3. From object values
type Count = (typeof stock)[Fruit]; // 5 | 3
// 4. The array stays usable at runtime
const count = sizes.length; // count = 3
const first: Size = sizes[0]; // first = "small"
const fruit: Fruit = "banana";
const bananas: Count = stock[fruit]; // bananas = 3
Three operators work together in (typeof sizes)[number]. First, the typeof operator, used in a type position, takes the type of the sizes variable, which is readonly [“small”, “medium”, “large”] thanks to as const. After that, the index [number] asks for the type of any element, which is the union of the three literals. Without as const, the array type would be string[] and the derived type would be plain string.
The as const array is read-only, so a call such as sizes.push(“tiny”) fails with Property ‘push’ does not exist on type ‘readonly [“small”, “medium”, “large”]’. The read-only array matches our intent, because the list of allowed values is fixed.
4. Narrowing Literal Types and Checking User Input
A literal union narrows with plain equality checks, so after size === “small” returns false, the compiler knows that size is one of the other two values. A switch on the value works the same way, and a never check in its default branch catches a forgotten case, as with any union type.
Data from outside the program, such as a query parameter or a JSON field, arrives as a plain string. Before we can pass it to a function that expects Size, we must check it at runtime.
type Size = "small" | "medium" | "large";
const sizes: readonly Size[] = ["small", "medium", "large"];
// 1. Equality check narrows the union
function label(size: Size): string {
if (size === "small") {
return "S"; // size: "small"
}
return size === "medium" ? "M" : "L"; // size: "medium" | "large"
}
const l = label("medium"); // l = "M"
// 2. Type guard for a plain string
function isSize(value: string): value is Size {
return (sizes as readonly string[]).includes(value);
}
const input: string = "large";
if (isSize(input)) {
const s = label(input); // s = "L", input: Size
}
// 3. Convert with a default
function toSize(value: string): Size {
return sizes.find((s) => s === value) ?? "medium";
}
const s1 = toSize("small"); // s1 = "small"
const s2 = toSize("tiny"); // s2 = "medium"
The cast in isSize() needs an explanation, because the includes() method of a Size array accepts only Size arguments, and calling it with a plain string does not compile.
error TS2345: Argument of type 'string' is not assignable to parameter of type '"large" | "medium" | "small"'.
Widening the array to readonly string[] for that one call is safe, because we only read from it, and the value is Size return type narrows the argument in the calling code. The find() version avoids the cast and returns a default for unknown input, which suits values such as query parameters.
5. One Entry per Literal With Record
A Record whose key type is a literal union must contain an entry for every literal. The result is a lookup table that the compiler checks for completeness, and it is often shorter than a switch statement. For example, a pizza shop keeps one price per size in a Record<Size, number>, so adding an extra-large size makes the compiler point at the price table.
type Size = "small" | "medium" | "large";
// 1. Every literal must have an entry
const prices: Record<Size, number> = {
small: 5,
medium: 7,
large: 9,
};
// 2. Lookup by a Size value
const size: Size = "medium";
const price = prices[size]; // price = 7
When we add a new size to the union, every Record<Size, …> without the new key stops compiling. Leaving out large makes the compiler name the missing key.
error TS2741: Property 'large' is missing in type '{ small: number; medium: number; }' but required in type 'Record<Size, number>'.
The return type of prices[size] is number, not number | undefined, because the compiler knows that every key of a TypeScript Record with a literal key type exists.
6. Literal Union or Enum?
TypeScript also has the enum keyword, and both features describe a fixed set of named values, but they differ in what they produce at runtime and in how values are passed around.
| Question | String literal union | enum |
|---|---|---|
| Runtime code | None; the type is removed | An object is generated |
| Passing a value | The plain string, “small” | The member, Size.Small |
| Works with Node.js type stripping | Yes | No, enums are not erasable syntax |
| List of values at runtime | Via an as const array | Via Object.values() |
For new code, a literal union, optionally derived from an as const array, is the simpler choice in most cases, because it produces no extra JavaScript, and plain strings from JSON fit it after a check. Enums still have their place, and TypeScript enums come in numeric and string forms as well as const enums.
7. Running the Literal Type Examples
The literal-types folder on GitHub has one file per section and an index.ts that runs them all. It requires Node.js 22 or newer and installs TypeScript 7.0.2.
npm install
npm start
Each section prints the values from the snippet comments. We kept the examples that fail to compile out of the project, so the build succeeds.
8. Conclusion
Literal types turn single values into types, and a union of them restricts a variable to a fixed list such as “small” | “medium” | “large”. A const variable keeps its literal type, while let variables and object properties widen to string unless we apply one of the fixes from section 2. Deriving the union from an as const array keeps the type and the runtime list in one place, and Record<Size, T> or an exhaustive switch makes the compiler report every place that must change when the list grows.
9. References
The TypeScript Handbook describes literal types and widening, and the as const syntax was added in TypeScript 3.4.
- TypeScript Handbook: Literal types
- TypeScript Handbook: Typeof type operator
- TypeScript Handbook: Indexed access types
- TypeScript 3.4 release notes: const assertions
- MDN: Array.prototype.includes()
Happy Learning !!