Annotations and inference
When to write the type out and when to let TypeScript work it out: inference, what a name that never changes is worth, and where the compiler has to be told.
You do not annotate everything
TypeScript reads your code before you run it, and it is good at working out what a value is. const count = 12; is a number, and it will stay a number — annotating it as : number adds nothing the compiler did not already know.
So the habit is not "annotate everything". It is: annotate where the compiler cannot know, and let it infer everywhere else. That keeps the code readable and the checks just as tight.
The distinction that matters is whether a name can be reassigned. A const is bound once, so the compiler can keep the narrowest type it saw. A let may be given another value later, so the inferred type is the general one — let count = 12; is still a number, and assigning a string to it is still an error.
How the checker reads a declaration
For a declaration without an annotation, the checker works from the inside out: it types the literal, then the operator, then the call, and finally gives the name the type that came back. For a declaration with one, it works the other way around. The annotation is the contract, and the value on the right has to fit inside it. That is why const total: number = 3 * 4; is accepted while const total: number = "12"; is not: the second value is a string, and a string does not fit a number. An annotation can be wider than the value, but never narrower, and it never converts anything — it only describes.
One of these needs help
const count = 12; // inferred: number
const label = "loaves"; // inferred: string
const names = []; // inferred: any[] — no help at all
function greet(who) { // who could be anything
return "Hello, " + who;
}Where inference stops
An empty list is the clearest case: with nothing inside it, there is nothing to infer, and TypeScript falls back to any — the type that switches checking off for that value. const names: string[] = []; says what will go in, and from then on every push is checked.
Function parameters are the other case. A function is written for every caller the compiler cannot see, so the parameter types are exactly the part you have to say out loud. In return, every call site is checked against them.
Types are checked, then erased
TypeScript does not run in the browser. The types are checked first, then removed, and the JavaScript that remains is what the page executes. That is why a type error stops the build even though the running program would never see a type.
The escape hatches are worth knowing. any switches checking off for a value; unknown says "not known yet" and forces you to narrow it with typeof or a check before using it. Prefer unknown at a boundary: it makes the check explicit instead of silent.
An assertion such as value as string does not convert anything; it only tells the compiler to stop asking. If the value is not a string at run time, the code still fails. Narrow with a real check when the input comes from outside your program.
Names for shapes
A type alias gives a name to a shape: type Point = { x: number; y: number };. The compiler checks the shape wherever the name is used, and the name explains the intent at every call site. An interface does the same job for objects and is the usual choice when the shape may be extended; for a quick alias, type is shorter.
Use a named type when the same shape appears twice. Repeating { x: number; y: number } in three signatures works, but changing the shape then means finding all three.
What goes wrong
The empty list is the quiet one. const names = []; compiles, because any[] accepts everything, and the mistake does not surface at the declaration. It surfaces later, where something assumes a string: names[0].toUpperCase() is allowed and can fail at run time. Hovering the name shows any[], and that is the signal to write string[] and let the compiler check every push.
The other common failure is a parameter the compiler cannot see a caller for. With noImplicitAny on, function greet(who) { is an error naming the parameter: Parameter ’who’ implicitly has an ’any’ type. The fix is one annotation, not a cast. A cast here would silence the message and leave the caller unchecked, which is exactly the trade you do not want.
Try these next
- Declare a
type Pointand write a function that takes one and returns its distance from zero. - Annotate an empty array with
string[]and push two names. - Write a function that takes
unknown, narrow it withtypeof, and log the result.
Help where it is needed
Annotate the parameter, give the empty list a type, then add one name to the list and log its length.
3 left to do
- The parameter says it takes text
- The list is typed as a list of text
- A name is added to the list
A list of text is string[] — the type of the items, then square brackets. The parameter takes its type the same way a variable does, after a colon.
From unknown to a checked value
Write a function that takes an `unknown` value, checks whether it is a string, and logs either the text or a message. Call it with a string and with a number.
1 left to do
- The parameter is unknown, not any
- The value is narrowed with typeof
- Both calls are logged
if (typeof value === "string") narrows value inside the branch. The other branch can log a fallback without pretending the value is text.
A quick way to see what was inferred: hover the name in an editor. If it says any, the compiler has given up — that is the spot that wanted an annotation.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does const names = []; infer, and why does it matter?
