intermediate20 min

Literal Types and const Assertions

Constrain a value to an exact set, and stop TypeScript widening the types you wanted narrow.

What you'll learn

  • Use a literal union to restrict a value to an exact set
  • Explain why an object property widens, and fix it with `as const`
  • Derive a union type from an existing object with `keyof` and `typeof`

Prerequisites

Explanation

A literal type is a type with exactly one possible value: "draft" is a type, and the only value it accepts is the string "draft". On its own that is useless. In a union it is one of TypeScript's sharpest tools:

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

"centre" is now a compile error rather than a silent no-op discovered by a user.

The widening problem

Lesson 2 introduced widening. Here is where it bites:

const config = { align: "center" };
// config.align is string, not "center"

The const applies to config, not to its properties — config.align can be reassigned, so TypeScript widens it to string. Passing config.align to something expecting Alignment is then rejected, which feels wrong until you see why.

as const

A const assertion freezes the whole structure into its narrowest form:

const config = { align: "center" } as const;
// config.align is "center", and readonly

Every property becomes readonly and every literal keeps its literal type. This is the standard fix, and it composes: as const on an array gives you a readonly tuple of literals rather than string[].

Deriving a union from data

Two operators let a type follow a value instead of being maintained alongside it.

  • typeof x in type position gives the type of the value x.
  • keyof T gives a union of T's keys.

Together:

const ROLES = { admin: 3, editor: 2, viewer: 1 } as const;

type Role = keyof typeof ROLES; // "admin" | "editor" | "viewer"

Read it inside-out: typeof ROLES is the object's type, keyof extracts its keys. Add a role to the object and the type updates itself. There is no second list to forget.

The as const matters here: without it, the values widen to number and you lose the exact levels — though keyof would still work for the keys.

as const is not a cast

as const narrows what a value is understood to be. It is not as SomeType, which asserts a type the compiler cannot verify and can be genuinely unsafe. Lesson 11 covers that distinction.

Example

Widening, the const-assertion fix, and a union derived from an object so the two can never disagree.

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

const widened = { align: "center" };
const narrowed = { align: "center" } as const;

function applyAlignment(a: Alignment): string {
  return "text-align: " + a;
}

// applyAlignment(widened.align); // rejected: string is not Alignment
console.log(applyAlignment(narrowed.align));

const ROLES = { admin: 3, editor: 2, viewer: 1 } as const;
type Role = keyof typeof ROLES;

function levelFor(role: Role): number {
  return ROLES[role];
}

console.log(levelFor("admin"), levelFor("viewer"));

Try it yourself

Remove `as const` and Run. The error shows exactly what widening cost you.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Define `type Size = "small" | "medium" | "large"`. Write `sizeInPixels(size: Size): number` returning 12, 16, and 24 respectively.

Checks: sizeInPixels is defined · small and medium map correctly · large maps to 24

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Create `const ICONS = { save: "💾", trash: "🗑" } as const`, derive `type IconName = keyof typeof ICONS`, then write `iconFor(name: IconName): string` returning the matching icon.

Checks: ICONS maps save correctly · iconFor is defined · iconFor returns the matching icon

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Expecting `const obj = { a: "x" }` to give `a` a literal type. `const` protects the binding, not the properties — that needs `as const`.
  • Maintaining a union and an object separately, then letting them drift. `keyof typeof` derives one from the other.
  • Confusing `as const` with `as SomeType`. The first narrows what is already there; the second asserts something the compiler cannot check.

Knowledge check

Knowledge check

1. What type does `config.mode` have in `const config = { mode: "dark" }`?
2. What does `keyof typeof ROLES` produce for `const ROLES = { admin: 1, viewer: 2 }`?
3. What does `as const` do to an array literal?

Takeaway

Literal unions make invalid values unrepresentable; `as const` stops TypeScript widening away the precision you wanted.

Summary

Literal types restrict a value to an exact set. Object properties widen by default, which `as const` prevents, and `keyof typeof` derives a union from an existing object so the two can never fall out of sync.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.