Let’s talk about one of TypeScript’s most deceptively simple superpowers: string literal types. You’ve probably written code where a variable could only be a handful of specific strings, like "left", "right", or "center" for alignment. In plain JavaScript, you’d just use a string and pray to the programming gods that no one types "leeft". You’d write a bunch of unit tests and runtime checks to handle the inevitable typos. It’s a tedious, error-prone mess.

TypeScript’s string literal types are your escape from that particular circle of hell. They let you define a type that can only be one of a specific set of string values. It’s not just any string; it’s this exact string or that exact string. The syntax is brilliantly straightforward: you just use a union type with the literal strings.

type Alignment = "left" | "right" | "center";

let myAlignment: Alignment;
myAlignment = "left"; // 👍 All good.
myAlignment = "centre"; // ❌ Compiler Error: Did you mean "center", you brilliant, Commonwealth-spelling genius?

Suddenly, the compiler becomes your hyper-vigilant code reviewer, catching those typos the instant you make them. The beauty is that this all happens at compile time. There’s zero runtime cost. The type Alignment is completely erased from your JavaScript, leaving behind plain, performant strings.

Why This Isn’t Just a Fancy Enum

You might be thinking, “This sounds like an enum.” And you’re not entirely wrong. But string literal unions have some distinct advantages. They are simpler. There’s no generated code, no awkward reverse mappings. They are just types. More importantly, they are transparent. The value of a variable of type Alignment is just a string—"left"—which means it plays perfectly with any existing JavaScript API that expects a string. You don’t have to do SomeEnum.SomeValue to get the underlying string. The value is the string.

The Pitfall: It’s Still Just a String at Runtime

Here’s the first “gotcha” I’ve seen bite people, and it’s a crucial one to understand. TypeScript’s type system is a figment of the compiler’s imagination. It doesn’t exist at runtime. This means if you get a string from an external source—user input, a network request, a config file—you cannot just cast it to your Alignment type and assume you’re safe.

// DANGER: This is a lie.
const badAlignment = JSON.parse('"left"') as Alignment; // Compiler says ✅
const worseAlignment = JSON.parse('"leeft"') as Alignment; // Compiler still says ✅! 😱

console.log(worseAlignment); // logs "leeft", which is NOT a valid Alignment.

The as Alignment cast is you telling the compiler, “Trust me, I know what this is.” The compiler, being a trusting soul, believes you. You just lied to it. To do this safely, you need a type guard—a runtime check that validates the string and helps the compiler understand the type.

// This is the correct, safe way.
function isAlignment(value: string): value is Alignment {
    return value === "left" || value === "right" || value === "center";
}

const untrustedInput = JSON.parse('"leeft"');
if (isAlignment(untrustedInput)) {
    // Now we're safe inside this block. TypeScript knows untrustedInput is Alignment.
    const goodAlignment: Alignment = untrustedInput;
} else {
    console.error(`Invalid alignment: ${untrustedInput}`);
}

Const Assertions: Locking It Down

Sometimes, the problem isn’t external input; it’s your own code being too helpful. Look at this:

const myConfig = {
    alignment: "left" // TypeScript infers the type as `string`
};

myConfig.alignment = "leeft"; // No error, because it's just a string.

We know "left" is meant to be our literal type, but TypeScript sees a string value and infers the broader string type. This is where a const assertion comes to the rescue. By appending as const, you tell TypeScript to infer the narrowest possible type.

const myConfig = {
    alignment: "left" // We want the literal type "left", not string.
} as const; // ← This is the magic sauce.

// Now, myConfig.alignment has the literal type "left", not string.
// myConfig.alignment = "leeft"; // ❌ Compiler Error: Cannot assign '"leeft"' to '"left"'.

The as const assertion makes the entire object deeply readonly and infers all its properties as their literal types. It’s incredibly powerful for creating immutable configuration objects with perfectly precise types. Use it whenever you have a fixed set of values that should never change. It’s one of those features that feels like you’re getting away with something.