TypeScript63 min total · 21 parts
TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime
Part 8 of 21 · ~2 min
Literal Types and as const
"screening" doesn't have to mean "any string" in TypeScript — it can be its own type, matching exactly that one value and nothing else. That's a literal type, and it's the natural fit for the queue's pipeline stage, so let's actually add the field:
type Stage = "screening" | "interview" | "offer" | "hired" | "rejected";
interface Applicant {
id: number;
name: string;
email: string;
stage: Stage;
}
let currentStage: Stage;
currentStage = "screening"; // fine
currentStage = "onboarding"; // Error: Type '"onboarding"' is not assignable to type 'Stage'
Here's where it gets less intuitive. Assign a string literal to a plain variable, and TypeScript doesn't keep the narrow type — it generalizes:
let label = "screening"; // widened: string
const label2 = "screening"; // stays literal: "screening" — const can't be reassigned, so there's nothing to widen for
You might expect that same narrowness to survive once the literal becomes a property of an object, but it doesn't, even under const:
const defaultFilters = { stage: "screening" };
// defaultFilters.stage is string, not "screening" — the object binding is const,
// but defaultFilters.stage = "offer" is perfectly legal, so TS has to leave room for it
function byStage(stage: Stage) { /* ... */ }
byStage(defaultFilters.stage); // Error — even though the actual value in the source is unmistakably "screening"
That error is disorienting the first time you hit it, because the value on the page is obviously correct — it's the type TypeScript inferred for it that's too loose, not the value itself. as const is how you tell TypeScript to stop hedging:
const defaultFilters2 = { stage: "screening" } as const;
// defaultFilters2.stage: "screening" — and the whole object is now deeply readonly, so there's
// no future reassignment for TypeScript to leave room for
byStage(defaultFilters2.stage); // fine
as const earns its keep anywhere a value is meant to stay pinned to its exact literal shape — a dropdown's list of valid options, an action object dispatched to a reducer, or an array you want TypeScript to treat as fixed-length rather than open-ended:
const scoreRange = [0, 100] as const; // readonly [0, 100] — a tuple, not number[]