Literal Types in TypeScript: Lock a Value to One Exact Choice

Learn how TypeScript literal types let a value be one exact thing, like "small" or 404, and how literal unions model a finite set of allowed options with full safety.

Published September 20, 20268 min readBy ACY Partner Indonesia
Literal Types cover with the code chip "on" | "off"
300 × 250Ad Space AvailablePlace your ad here

Most of the time when you write TypeScript, you describe values in broad strokes. A name is a string, an age is a number, a flag is a boolean. That works, but it is loose. A string can be "hello", "goodbye", or the entire text of a novel. Often the value you actually care about is far narrower than that. A t-shirt size is only ever "small", "medium", or "large". An HTTP response is 200, 404, or 500, not just “some number”.

Literal types are TypeScript’s way of saying: this value is not merely a string or a number, it is this exact one. Once you learn to reach for them, your types stop describing rough categories and start describing the precise reality of your program. Let’s walk through what they are and why they make your code so much safer.

What is a literal type?

A literal type is a type made of a single, specific value. Instead of the broad type string, you can have the type "small" — a type that allows only the exact text "small" and nothing else.

let size: "small";

size = "small"; // fine
size = "large"; // Error: Type '"large"' is not assignable to type '"small"'

Read that slowly. The type of size is the word "small" itself. The only value that fits is "small". Even "Small" with a capital S would be rejected, because it is a different string.

The same idea works for numbers and booleans:

let status: 200;   // only the number 200
let isReady: true; // only the boolean true

On its own, a single literal type is not very useful — a variable that can only ever be one value is just a constant. The power shows up when you combine literals into a union.

Modelling a finite set with literal unions

A union type uses the | symbol to mean “one of these”. When you union several literal types together, you describe a complete, closed list of allowed values.

type Size = "small" | "medium" | "large";

let shirt: Size;

shirt = "medium"; // fine
shirt = "extra-large"; // Error: not assignable to type 'Size'

Now Size is a type that means exactly one of three strings. There is no fourth option. If someone tries to assign anything outside that set, TypeScript stops them before the code ever runs.

This is enormously useful because so many real-world values are finite sets. Think about how often you see something like this:

  • An order status: "pending" | "shipped" | "delivered" | "cancelled"
  • A theme: "light" | "dark"
  • A log level: "debug" | "info" | "warn" | "error"
  • An HTTP status: 200 | 301 | 404 | 500

Before literal types, you would probably type all of these as string or number and just hope nobody passed a typo. With literal unions, the typo becomes a compile-time error.

A literal union is a tiny enum

If you have used enums in other languages, a string-literal union covers most of the same ground with less ceremony. You get a fixed list of named options, full editor autocomplete, and no extra runtime code. Many TypeScript teams prefer literal unions over enums for exactly this reason.

Why this is so good for safety

The real win is that literal types push mistakes from runtime to compile time. Consider a function that sets a button’s state.

type ButtonState = "idle" | "loading" | "success" | "error";

function setButton(state: ButtonState) {
  // ...
}

setButton("loading"); // fine
setButton("loadng");  // Error: caught instantly, typo never ships

Without the literal union, the parameter would be string, and "loadng" would sail straight through. You would only find the bug later, maybe in production, maybe never. With it, the editor underlines the mistake the moment you type it.

There is a second, quieter benefit: autocomplete. Because TypeScript knows the full set of allowed values, your editor can offer them as suggestions. You do not have to remember whether it is "cancelled" or "canceled" — you just pick from the list. The type becomes living documentation.

Exhaustiveness checking

Literal unions also let TypeScript verify that you handled every case. Pair a union with a switch, and you can ask the compiler to prove that nothing slipped through.

type Size = "small" | "medium" | "large";

function priceFor(size: Size): number {
  switch (size) {
    case "small":
      return 10;
    case "medium":
      return 15;
    case "large":
      return 20;
    default:
      // If a new size is ever added to the union and not handled
      // above, this line becomes a type error.
      const unreachable: never = size;
      return unreachable;
  }
}

The never trick is a small but powerful pattern. The type never is the type of a value that can never exist. If you have handled every member of the union, size inside default is narrowed to never, and the assignment is fine. The day someone adds "extra-large" to Size, that assignment breaks, and the compiler points you straight at the code you forgot to update.

The catch: how TypeScript infers literals

Here is where many beginners get tripped up. When you declare a variable with let, TypeScript usually does not infer a literal type. It infers the broader type instead.

let mode = "dark";
// inferred type: string (not "dark")

Why? Because let means the variable can be reassigned. Since you might later write mode = "light", TypeScript widens the type to string so that future assignments are allowed. This widening is helpful most of the time, but it gets in your way when you actually wanted a literal.

Compare that with const:

const mode = "dark";
// inferred type: "dark" (a literal!)

A const can never be reassigned, so TypeScript is free to lock the type to the exact value. This is called literal inference, and the rule of thumb is simple: const gives you a literal type, let gives you the widened type.

const mode = "dark"   ->  type is "dark"   (literal, narrow)
let   mode = "dark"   ->  type is string   (widened, broad)

Forcing a literal where you need one

Sometimes you build an object and want its string fields to stay as literals, but object properties are widened just like let variables.

const config = { theme: "dark" };
// config.theme is string, not "dark"

You have two main ways to keep the literal. The first is a type annotation that constrains the value:

type Theme = "light" | "dark";

const config: { theme: Theme } = { theme: "dark" };
// config.theme is now Theme, and only "light" or "dark" are allowed

The second is the as const assertion, which tells TypeScript to treat the whole thing as deeply read-only and to keep every value as its narrowest literal type:

const config = { theme: "dark" } as const;
// config.theme is "dark", and the object is readonly

as const is especially handy for arrays of options you want to reuse as a type:

const sizes = ["small", "medium", "large"] as const;
type Size = (typeof sizes)[number];
// Size is "small" | "medium" | "large"

That last example is a neat trick: you write the list of values once, and derive the type from it. No risk of the array and the type drifting apart over time.

Watch out for accidental widening

If your literal type “mysteriously” became a plain string or number, widening is almost always the cause. Check whether you used let instead of const, or whether the value lives inside an object or array without as const. The fix is usually one of those two annotations.

A small, realistic example

Let’s tie it together with something close to real code. Imagine a function that creates a notification. Each notification has a level and a matching icon.

type Level = "info" | "success" | "warning" | "error";

interface Notification {
  level: Level;
  message: string;
}

function iconFor(level: Level): string {
  switch (level) {
    case "info":
      return "ℹ️";
    case "success":
      return "✅";
    case "warning":
      return "⚠️";
    case "error":
      return "❌";
  }
}

const note: Notification = {
  level: "success",
  message: "Profile saved",
};

console.log(iconFor(note.level)); // ✅

Everything here is checked. You cannot create a Notification with level: "urgent", you cannot call iconFor("urgnt"), and if you later add a "debug" level, TypeScript will remind you to give it an icon. The set of valid states is written down once, and the compiler enforces it everywhere.

Recap

Literal types let you describe a value as one exact thing instead of a broad category. On their own they model a single constant, but combined with the | union operator they shine: they let you spell out a finite, closed set of allowed values such as "small" | "medium" | "large" or 200 | 404 | 500.

The payoff is safety and clarity. Typos become compile-time errors, your editor offers autocomplete from the allowed set, and patterns like the never exhaustiveness check make sure you never forget a case. Just keep the inference rule in mind: const preserves literals, let widens them, and as const lets you lock down objects and arrays when you need the narrow type. Reach for literal unions whenever your data has a fixed list of valid options, and let the compiler hold you to it.

Tags:typescriptliteral typesunion typestype safetyfrontend
728 × 90Ad Space AvailablePlace your ad here

Related Articles

See All Articles

You Might Also Like

Frontend best practices overview cover
Frontend / Fundamentals

Frontend Best Practices: An Overview

A friendly map of what good frontend really means — semantic HTML, clean CSS, unobtrusive JavaScript, responsive layouts, performance, and progressive enhancement, with pointers to go deeper.

Sep 20, 20268 min read
A Frontend Developer Roadmap cover with the path HTML to CSS to JavaScript
Frontend / Fundamentals

A Frontend Developer Roadmap: What to Learn, and in What Order

A calm, step-by-step learning path for beginners: HTML, CSS, JavaScript, developer tools, responsive and accessible design, a framework, TypeScript, and deployment, with the reasoning behind each step.

Sep 20, 202611 min read
Title card reading Styling and Interactivity over a dark blue ACY Partner background
Frontend / Fundamentals

How Styling and Interactivity Work

A beginner-friendly look at how CSS turns plain HTML into a designed layout, and how JavaScript adds behavior — the structure, style, behavior mental model for building web pages.

Sep 20, 20269 min read
Three front-end layers combining on one web page
Frontend / Fundamentals

How HTML, CSS, and JavaScript Work Together

A hands-on walkthrough for beginners: build one small button, give it structure with HTML, looks with CSS, and behavior with JavaScript, and watch the three layers cooperate on a real page.

Sep 20, 20268 min read
The Frontend Developer Toolkit cover with editor, browser, and CLI motifs
Frontend / Fundamentals

The Frontend Developer Toolkit

A friendly tour of the everyday tools a frontend developer relies on — code editor, browser and DevTools, terminal, version control, package managers, and a dev server — and what each one is actually for.

Sep 20, 202610 min read
Cover illustration for What Frontend Developers Do
Frontend / Fundamentals

What Frontend Developers Do: A Beginner's Guide to the Role

A clear, beginner-friendly look at what frontend developers actually do every day: turning designs into working interfaces, building reusable UI, and caring about responsiveness, accessibility, and speed.

Sep 20, 202610 min read