Give every value an ID card
string, number, boolean — a type is just the label a value carries with it. The useful part is that you rarely have to write the label yourself. TypeScript looks at the value and fills it in. That is called inference.
Every value gets an ID card
The whole chapter grows out of this one comparison.
The stockroom
You run a tea shop. The stockroom is full of boxes, and every box has a label: "boba, bagged, 5 kg", "coconut milk, canned, perishable". You know what each box can and cannot be used for without opening it.
TypeScript puts the same kind of label on every value in your program. The label is called a type: the name field is a string, the price field is a number. Every time you use the value, the compiler reads the label first. If you try to multiply a string, the label does not allow it and you get an error.
You never write most of these labels. Type let price = 22 and TypeScript reads the 22 on the right and fills the label in. That is called inference. You only write the label by hand in a few places, which is called an annotation. Section 04 says exactly where.
The real things in the program: 22, "Oolong Tea", an order object. These are what the program actually works with when it runs.
A description of a value: what it is, which fields it has. Types exist only while the compiler runs. The compiler removes every annotation, type, and interface and emits plain JavaScript, so nothing checks types at runtime unless you write that check yourself.
It fills in labels (inference) and reads them (checking). When an operation does not match the label, it reports an error before the code ever runs.
Primitive types: the seven basic ones
You meet string, number, and boolean every day. null and undefined are two different ways of saying there is no value. bigint and symbol are rare, but worth recognizing. Click a value to see its type.
A sequence of characters. Single quotes, double quotes, and backticks all produce the same type: string.
Learn the first five well, and just recognize the last two
Almost all everyday code deals with string, number, boolean, null, and undefined. The difference between null and undefined is worth one sentence: undefined is the value you get when nothing was ever assigned (no initial value, a missing property, a function that returns nothing), and null is a value someone assigned on purpose to mean "empty".
They are separate types only because strictNullChecks is on. Turn that flag off and null and undefined can be assigned to every type, so the compiler stops catching this whole class of mistake. Chapter 10 covers the flag itself.
Containers: arrays, tuples, and object shapes
A single value has a type, and so does a container of values. An array type names the element type. A tuple also fixes the length. An object type names the type of each property.
string[] is read as "array of string". Array<string> means exactly the same thing. For simple element types most code uses the first form because it is shorter.number[] says nothing about how many elements there are. A tuple such as [string, number] says there are exactly two, and says what each one is. One surprise: push on a tuple is still allowed, because a tuple is still an ordinary array at runtime. The fixed length is checked when you build the value and when you index into it, not when you mutate it later. Add readonly if you need the mutation blocked too.{ name: string; price: number } is itself a type, written inline. It is called an object type literal, and the properties are separated by semicolons. Shapes nest and go into arrays, which is how almost all real data gets described. Optional properties (?), interface, and type aliases are the whole of the next chapter.Annotation vs inference: when do you write the type yourself?
An annotation is a promise. Inference is an observation. First look at how much the compiler works out on its own.
A let variable can be assigned again later, so TypeScript records the wider type: "some string". Going from the literal type to string is called widening.
For a local variable that is assigned right where it is declared, inference covers everything. So where do annotations belong? At the boundaries: anywhere other code depends on the type, and anywhere inference has nothing to read.
A parameter comes from outside, so inference has nothing to read. Under noImplicitAny you have to write it. A return type can be inferred, but writing it makes the promise explicit, which is worth doing for exported functions.
Exported constants, a menu structure the whole app reads: give them a named type. It becomes the contract, and anyone who puts the wrong thing in gets an error at that line.
At the moment of declaration there is no value on the right, so there is nothing to infer from. You have to say what this variable will hold.
Literal types and widening: a type can be one exact value
The let / const difference from the previous section deserves a closer look. It is the entry point to unions in chapter 03.
A type can be wide, meaning any string at all. It can also be narrow, meaning exactly the string "small" and nothing else. The narrow one is called a literal type. Which one you get depends on how the variable was declared. let relaxes the literal into string, and that step is called widening. const keeps the literal. The reason is simple: a let variable is meant to be reassigned, so a type that only allows one value would be unusable.
"small" | "medium" | "large" is read as "one of these three". That is a union of literal types: the list of allowed values is written into the type itself, so a wrong size is rejected when you save the file. The vertical bar | is the subject of chapter 03.const locks the variable, not the contents
const size = "small" has the literal type "small". But const sizes = ["small"] is string[], and const d = { price: 22 } is { price: number }. Array elements and object properties still widen, because const only prevents reassigning the variable name. The contents can still change.
To keep the literal types inside a structure, write as const: const d = { price: 22 } as const has the type { readonly price: 22 }. The final chapter covers it.
Why is this so much better than string?
If a cup size is typed as string, then "mega", "Large", and "LARGE" are all legal, and a typo still costs you at runtime. With a union of literals, anything outside the list is rejected at compile time. The narrower the set of legal values, the more the compiler can catch for you. This idea runs through the whole course and chapter 03 uses it heavily.any: the switch that turns checking off
any does not mean 'any type'. It means 'do not check this'. And it spreads to whatever the value touches.
prise is misspelled, and nothing reports it. Then total becomes any as well, so calling toUpperCase() on a number is also accepted. Both mistakes survive until the program runs, exactly as they would in plain JavaScript.Two things any does to your code
It turns checking off. Once a value is any, the compiler accepts any operation on it: a misspelled property, a method that does not exist, a wrong argument. All silent.
It spreads. A property read from an any value, a result computed from it, a callback parameter it is passed to — all become any. One any can quiet an entire chain of code.
This does not mean you must never use it. It is useful while migrating an old JavaScript project, and sometimes as a short-term escape (the final chapter discusses when it is reasonable). The rule is: use it deliberately, and know what you switched off. There is also a safe alternative, unknown: it accepts any value, but it lets you do almost nothing with that value until you check what it is. Chapter 03 covers it.
There is also an any you did not write: an implicit one. An unannotated function parameter is the most common source. The compiler option noImplicitAny, part of the strict family, reports every place where a type silently falls back to any. The TypeScript Playground has strict on by default, so you will see this option working in the practice tasks.
Case study: typing the menu
The first episode of the running example. One case shows what inference handles and what an annotation is for.
prise is recorded as a real field. Nothing is reported until filter reads price, and the error then points at code that did nothing wrong.MenuItem contract in place, the error lands on the line with the typo, and it even suggests the fix. That is what "annotate at the boundary" buys you: the place that reports the error is the place that contains the mistake.What this episode showed
Leave local variables to inference, and write a contract for shared data. TheMenuItem shape follows us through the rest of the course: chapter 03 adds the Size union and an order status, chapter 05 puts it inside a generic container, and chapters 06 and 07 reshape it with utility types.Three beginner mistakes, fixed now
All three show up repeatedly in real projects. One minute each.
Mistake 1: String is not string
CapitalString is the type of the wrapper object created by new String(), not the primitive string. A String is not assignable to a string, so using it produces confusing errors. Annotations always use the lowercase names: string, number, boolean. Treat String, Number, and Boolean as if they did not exist.Mistake 2: annotating everything adds noise, not safety
Inlet count: number = 0 the annotation repeats what the compiler already read from the 0. It takes up space and it buries the annotations that actually matter. Let inference handle the obvious cases and keep annotations for the three boundaries in section 04. Then a reader can quickly see where the contracts are.Mistake 3: an empty array becomes any[]
An empty array has no elements, so there is nothing to infer the element type from, and TypeScript recordsany[]. It then tries to work the type out from later push calls in the same scope, which is called an evolving array. That guess stops working as soon as the value leaves the function or is used before it is filled, and noImplicitAny reports it. Give the empty box a label when you create it.toppings line produces no error at all; its type is simply any[]. One annotation, const safe: string[] = [], removes the whole problem, and every later push is checked.Practice
Five tasks, all in the TypeScript Playground, about fifteen minutes. For inference, hovering once teaches more than reading ten times.
Chapter quiz
Eight questions covering inference, widening, any, and the empty array. Answer all of them correctly to light the green dot in the sidebar.
let price = 22; — what is the type of price?
const size = "small"; — what is the type of size?
: String (capital) and : string (lowercase) — how are they related?
Where is it worth writing (or required to write) a type annotation? Select all that apply.
Which statement about any is correct?
"An array whose elements are all number", written with the square-bracket syntax, is the type ____.
const toppings = []; — what is the problem with this line?
let size = "small" is inferred as string, but you want size to accept only the three cup sizes. What is the right way?
- A type is the label a value carries. Inference means the compiler reads that label for you, so most local variables need no annotation at all. Types are removed during compilation: nothing checks them while the program runs.
- Seven primitive types. You use string, number, boolean, null, and undefined daily; bigint and symbol are rare. undefined means "never assigned", null means "deliberately empty", and
strictNullChecksis what keeps them separate from every other type. - An annotation is a promise, inference is an observation. Write annotations at the boundaries: function parameters and return types, shared and exported data, and variables declared before they are assigned.
letwidens ("small"becomes string),constkeeps the literal type. Butconstdoes not lock the contents: array elements and object properties still widen unless you writeas const. Unions of literals ("small" | "medium" | "large") lead into chapter 03.anyturns checking off and spreads through expressions; an empty[]starts asany[]. Both are fine as a temporary step and bad as a permanent state.noImplicitAnyreports the ones you did not write yourself.