Now, let’s get literal. You’ve seen how TypeScript lets you slap a string type on a variable and call it a day. But sometimes, you don’t just want a string; you want that specific string. You don’t want a number; you want the number 42. This is where literal types come in, and they are the secret sauce for writing code that’s not just type-safe, but *value-safe`.

Think of a literal type as locking a variable to one specific, allowed value. It’s the difference between saying “this must be a road” and “this must be the I-95, and heaven help you if you end up on the I-93 instead.”

The Basics: String, Number, and Boolean Literals

You create them exactly how you’d expect: by assigning a constant value. TypeScript sees that the value can’t change and infers the most specific type possible—the literal itself.

// The type of `direction` is NOT `string`. It's the literal type "left".
let direction = "left";

// The type of `answer` is NOT `number`. It's the literal type `42`.
const answer = 42;

// The type of `isActive` is NOT `boolean`. It's the literal type `true`.
const isActive = true;

The real power, however, unlocks when you use them in a union type. A single literal is, frankly, not very useful on its own. But a union of several literals? Now you’ve got an enum-like structure that’s built right into the type system.

// This isn't a string. It's a type that can only be one of these four values.
type CardinalDirection = "north" | "south" | "east" | "west";

function move(direction: CardinalDirection) {
  // ... implementation
}

move("north"); // 👍 All good!
move("N orth"); // ❌ Error: Argument of type '"N orth"' is not assignable to parameter of type 'CardinalDirection'.

Why This Isn’t Just a Fancy Enum

I can hear you thinking, “Cool, but I’ll just use an enum.” Don’t. Well, not always. String literal unions have some compelling advantages. They are simpler. They generate no extra code in your compiled JavaScript—they’re just strings. They play nicely with existing string-based APIs. And you don’t have to import them; a plain old string is all you need. It’s type safety without the ceremony.

The Pitfall: It’s Just a Type

Here’s the first “gotcha.” This is a type system feature, not a runtime value. You can’t loop over the members of CardinalDirection at runtime. If you need to do that, you need to create a parallel array of the actual values. This is a bit of a pain, and it’s where the designers arguably left a rough edge.

// This is a type. It vanishes at runtime.
type Direction = "up" | "down";

// So you need a runtime counterpart if you want to iterate.
const directions = ["up", "down"] as const; // We'll get to 'as const' in a second!

function isValidDirection(str: string): str is Direction {
  // We have to check against the runtime array, not the type.
  return (directions as readonly string[]).includes(str);
}

Const Assertions: Forcing the Issue

What if you have an object? By default, TypeScript widens the types of its properties. It’s trying to be helpful, assuming you might want to change those values later.

const userPreferences = {
  theme: "dark", // TypeScript infers `string` here. Helpful but wrong.
  animations: true // Infers `boolean`, not `true`.
};

This is where we use a const assertion—my favorite little spell in the language. By appending as const, you tell TypeScript: “Everything in this object is read-only, and I want the literal types, please and thank you.”

const userPreferences = {
  theme: "dark",
  animations: true
} as const;

// Now, the type of `userPreferences.theme` is literally "dark"
// and the type of `userPreferences.animations` is literally `true`.

This is incredibly powerful for defining configuration objects, frozen state, or any other data structure you want to be deeply immutable and hyper-specific at the type level. It’s like Object.freeze() on steroids, but for your types.

The Double-Edged Sword of Specificity

The honesty hour: this specificity can sometimes be too good. Let’s say you have a function that accepts a literal type.

function setTheme(theme: "light" | "dark") { ... }

If you try to pass it a variable, even if you know that variable has a valid value, TypeScript will freak out. It sees the variable as a string, which is wider than your literal union. This is the correct behavior—the variable could change!

const myTheme = "dark";
setTheme(myTheme); // ❌ Error! Type 'string' is not assignable to type '"light" | "dark"'

The fix is to help TypeScript narrow the type. You can do this by asserting the type yourself (setTheme(myTheme as "dark")), but that’s clunky. The better way is to tell TypeScript that myTheme itself is of the literal type from the very beginning.

// Solution 1: Type the constant explicitly
const myTheme: "dark" = "dark";

// Solution 2 (better): Use a const assertion on the value itself
const myTheme = "dark" as const;

// Both now work perfectly.
setTheme(myTheme); // 👍

This is the trade-off. You get phenomenal, precise control, but you have to be intentional about how you define your values. It forces you to think like the type system, which is ultimately what makes you a better TypeScript developer. You’re not just writing JavaScript with types; you’re sculpting the type graph itself.