Right, so you’ve just started using literal types and you’re feeling clever. You’ve written const direction = 'left' and you see that beautiful type 'left' instead of string in your editor. You think, “I’ve got this. TypeScript understands exactly what I mean.” And then you do this:

let myDirection = 'left';
//    ^? let myDirection: string

Wait, what? Why is it string now? I just told it it was 'left'! Welcome to your first encounter with TypeScript’s slightly overzealous but well-intentioned habit: literal type widening. It’s the language’s way of saying, “I trust you… but not that much.”

Why Would TypeScript Do This To Me?

It all comes down to mutability. The let keyword is your first clue. You’re not promising this variable will always be 'left'; you’re just setting its initial value. TypeScript sees you using let and thinks, “Okay, this human might want to change this value later. I’ll give them a string to play with so they don’t get angry type errors when they try to assign 'right'.”

It’s a sensible default. The alternative would be inferring let myDirection: 'left', which would then scream at you the moment you tried to assign any other string. That would be incredibly annoying for the 99% of cases where you do intend to change the value. This behavior is the pragmatic choice for writing code that doesn’t fight you at every turn.

Where Widening Happens (And Where It Doesn’t)

The key to understanding this is context. TypeScript is constantly asking, “How permanent is this value supposed to be?”

// const declaration: The value is a hard constant. No widening.
const permanentId = 'abc-123'; // type is 'abc-123'

// let declaration: The value might change. Widen.
let temporaryId = 'abc-123'; // type is string

// In an object or array? The individual properties might change.
const obj = { counter: 0 };
//    ^? const obj: { counter: number }

const arr = ['hello', 'world'];
//    ^? const arr: string[]

See the pattern? With const, the binding itself is constant, so the literal can be trusted. With let, it can’t. And inside structures like objects and arrays, the contents are mutable even if the structure itself is a const, so TypeScript plays it safe and widens those too. It’s a cautious architect, always assuming you might want to renovate later.

The Escape Hatch: const Assertions

Sometimes, you know better. You have a let variable or an object literal that you swear won’t change, and you want TypeScript to treat it with the respect it deserves. This is where you look TypeScript in the eye and say, “No, really. I mean it.” You do this with a const assertion.

// Let's say we're setting a config flag that should never change at runtime.
let debugMode = true as const;
//    ^? let debugMode: true

// Now for the really powerful part: using it on object literals.
const primaryColors = {
  red: [255, 0, 0],
  green: [0, 255, 0],
  blue: [0, 0, 255],
} as const;
// ^? const primaryColors: { readonly red: readonly [255, 0, 0], ... }

The as const assertion tells the type system to go as narrow as possible. Every property becomes readonly, and every literal is locked in. It’s the difference between a friendly suggestion and carving your types into stone. You’re effectively turning that entire object structure into a deeply immutable value, which is incredibly powerful for defining config objects, API response shapes, or any other data that should be treated as a fixed truth.

Common Pitfalls and When to Fight It

The biggest gotcha is trying to use a widened type where a literal is expected.

function setTheme(theme: 'light' | 'dark') { /* ... */ }

let userTheme = 'light'; // type is string, sadly
setTheme(userTheme);
//       ~~~~~~~~~ Error: Argument of type 'string' is not assignable to parameter of type '"light" | "dark"'.

You’ve got three clean solutions:

  1. Annotate directly: let userTheme: 'light' | 'dark' = 'light';
  2. Use a const assertion: let userTheme = 'light' as const;
  3. Just use const: If it truly won’t change, const userTheme = 'light'; is the most honest and simplest choice.

The best practice? Don’t fight the widening unless you have a reason. Use it to your advantage. Let TypeScript widen things by default to make your code flexible. Only reach for as const or explicit type annotations when you need the extra precision—usually when defining immutable constants or when the type is required to flow correctly through your application’s logic. It’s a tool, not a crutch. Use it to tell the compiler what you know to be true, not to silence it for what you hope might be true.