"A type isn't a category. It's the precise set of values you'll accept. 'red' is a perfectly good type all by itself."
The shift in thinking
Most programmers learn typing through statically-typed languages where a type is a category — "this is a number," "this is a string." TypeScript can do that, but it can also do something stronger: a type can be a single value. type Status = 'idle' means the type that contains exactly one value, the string 'idle'. Anything else fails.
Combine literal types with unions and you get something like enums — but more flexible and erased at compile time. type Status = 'idle' | 'loading' | 'success' | 'error' describes the exact set of allowed values for a status field. Pass anything else and the compiler stops you.
How inference picks literal vs widened
This is the rule TypeScript uses to decide whether to give you a literal type or its widened parent:
const x = 'hi'infersx: 'hi'(literal —constcan't change).let x = 'hi'infersx: string(widened —letcould become any string).const x: 'hi' = 'hi'annotated explicitly — literal type held even if you'd otherwise widen.const arr = ['hi']infersarr: string[](array elements widen by default).const arr = ['hi'] as constinfersarr: readonly ['hi'](theas constkills both widening and mutability).
This widening behavior is why as const exists — it's the override knob to keep literal types in places where TypeScript would otherwise generalize.
Three categories of literal types
String literals: type Color = 'red' | 'green' | 'blue'. The most common form. Used for status fields, tag names, kind discriminators in unions.
Number literals: type Quadrant = 1 | 2 | 3 | 4. Less common but useful for bounded numeric domains. HTTP status codes are a natural fit: type Status = 200 | 404 | 500.
Boolean literals: type True = true. Rare on their own, but they show up in conditional types and in flags that you want to nail down.
string and start describing it with the precise set of values that actually appear. The compiler can then prove your code only handles cases that can actually happen.Literal types make narrowing strong
The biggest reason to learn literal types early: they make the narrowing track feel obvious later. When status is typed 'idle' | 'loading' | 'done', you can if (status === 'idle') and inside that branch TypeScript knows status is the literal 'idle' — and the other branches narrow to the remaining members. This is the foundation for discriminated unions, exhaustive switches, and most of the patterns in tracks 6 and 7.
Pippa's confession
BrainName is 'claude' | 'codex' | 'gemini' | 'ollama'. ReasoningLevel is 'minimal' | 'low' | 'medium' | 'high' | 'ultra'. The status fields on every async hook are literal unions. None of these are enums — they're just literal-type unions, and they erase at compile time so the runtime sees plain strings. The type system holds the constraints; the runtime stays light.