Types can be exact values
A literal type is a type with exactly one allowed value. "left" (the type) accepts only "left" (the string). Alone that is not useful, but combined with | (or) it models a fixed menu of options:
let direction: "left" | "right" = "left"; direction = "right"; // fine direction = "up"; // error: not assignable
Compare the JavaScript habit of passing magic strings and hoping nobody misspells "rigth". The | symbol builds a union type, and Unit 4 is devoted to it. For now, remember: strings and numbers themselves can be types, and a union of literals is TypeScript's built-in answer to "one of these exact values".
A variable restricted to two strings
let direction: "left" | "right" = "left"; console.log("moving " + direction); direction = "right"; console.log("moving " + direction);
Output
moving left moving right
The type here is the pair of quoted values themselves, so only those two exact strings are assignable. Both reassignments above are legal because each one picks a member of that pair.
Writing direction = "up" fails, and the error message lists the allowed values, which makes it self-documenting. That is the practical payoff over the plain JavaScript habit of passing magic strings: a misspelling like "rigth" is caught immediately rather than silently falling through an if chain at runtime.
any and unknown: checking off versus checking kept on
Two special types describe "could be anything", with opposite safety properties:
any | unknown | |
|---|---|---|
| Assign anything to it | yes | yes |
| Call methods on it | yes, unchecked | no, error |
| Type checking | switched off | still on |
any turns the compiler off for that value. The expression x.foo.bar() compiles happily and then crashes at runtime, which is exactly the class of bug you came here to escape.
unknown also accepts any value, but it keeps checking on. Every use is an error until you prove what the value is with a check such as typeof value === "string".
Values genuinely do have unknowable types when they come from outside your program. The classic source is a web API, a program on another server that yours sends a request to over the network and gets data back from, usually as JSON text. JSON.parse turns that text into a value, but nothing guarantees the other server sent the shape you expect. Type the result unknown and prove its shape before use. Unit 7 builds that full pattern.
Narrowing an unknown before using it
This function accepts unknown and proves the value is a string before touching .length.
function describe(value: unknown): string { if (typeof value === "string") { return "a string of length " + value.length; } return "not a string"; } console.log(describe("hello")); console.log(describe(42));
Output
a string of length 5 not a string
Where the proof applies
- Inside the
if, TypeScript treatsvalueas astring, so.lengthis allowed. Outside it,valueis stillunknownand.lengthwould be an error. - Moving the
return "a string of length " + value.lengthline above theifmakes the compiler refuse it, because at that point nothing has established whatvalueis. The check is not decoration, it is what earns the access. - Both calls compile because
unknownaccepts any argument. It is the use inside the function that is restricted, not the call.
flip: a function typed with a two-value union
flip(state: "on" | "off") returns the other value of the pair, and the variable it operates on carries the same union type.
function flip(state: "on" | "off"): "on" | "off" { if (state === "on") { return "off"; } return "on"; } let bulb: "on" | "off" = "off"; bulb = flip(bulb); console.log("bulb is " + bulb); bulb = flip(bulb); console.log("bulb is " + bulb);
Output
bulb is on bulb is off
Three things the types are buying here
- The return type is the same two-value union as the parameter, so a stray
return "broken"inside the function would not compile. The function cannot produce a state that callers are unprepared for. - Literal types are still ordinary strings at runtime. The comparison
state === "on"is exactly the JavaScript comparison you already know, and nothing about the union survives into the emitted code. bulbmust also be annotated"on" | "off"so it can hold both values over its lifetime. Annotating it as just"off", or lettingconstinfer that, would make the first reassignment an error.
Typing the result of JSON.parse
When your program parses text received from a web API and you do not yet know the shape, the right type for the variable is unknown.
unknown accepts the mystery value while keeping type checking on, so the compiler forces you to prove the shape before using it. That is the whole reason to prefer it here.
Two alternatives are worth ruling out explicitly:
anyalso accepts the value but disables checking, which reintroduces the silent runtime crashes TypeScript exists to prevent. It converts a compile-time question into a production incident.stringis wrong becauseJSON.parsereturns the text turned into data, which could be an object, an array, a number, or a boolean. The string was the input, not the result.