Chapter 3 of 24 · TypeScript for Beginners — Complete Free Tutorial
Basic Types and Annotations
With a working toolchain, the TypeScript for Beginners series can finally start annotating code. This chapter covers the primitive annotations you will type a hundred times a week, how null and undefined behave under strict mode, the power of type inference — why you usually write fewer annotations than expected — the handful of places an annotation is genuinely required, and how to read the error messages the compiler sends back.
Annotating primitives
An annotation is a colon and a type name: value: Type. The primitives mirror JavaScript's runtime values, with one important naming decision — TypeScript has no int or float, just number:
| Type | Example values | Notes |
|---|---|---|
string | "Ada", `Hi ${name}` | Text, always with double, single or backtick quotes |
number | 42, 3.14, 0x1f | Integers and floats alike; there is no separate integer type |
boolean | true, false | Never "true" — that is a string |
bigint | 9007199254740993n | Whole numbers beyond JavaScript's safe integer range |
symbol | Symbol("id") | Unique, immutable keys, mostly for library authors |
let username: string = "Ada";
let age: number = 36;
let active: boolean = true;
username = 42;
Expected output:
error TS2322: Type 'number' is not assignable to type 'string'.
The right-hand side is judged against the annotation, not against the value the variable happened to hold earlier. That single rule prevents an enormous family of bugs at almost no cost.
null and undefined
JavaScript has two “empty” values, and TypeScript keeps them distinct: undefined means never assigned, while null usually means deliberately empty. Under strict mode — which this whole series uses — neither is smuggled into other types:
let missing: undefined = undefined;
let empty: null = null;
let name: string = null;
Expected output:
error TS2322: Type 'null' is not assignable to type 'string'.
That third line is the payoff of strictNullChecks: a string is a string, full stop. When a value really can be absent you say so honestly, using a union — the subject of Union and Intersection Types:
let nickname: string | null = null;
nickname = "Ada"; // fine
nickname = nickname.toUpperCase(); // compiler still asks you to check first
Inference: when you can leave types out
You rarely annotate a plain variable, because the compiler infers the type from the value you gave it:
let count = 5; // inferred: number
let label = "chapters"; // inferred: string
count = "five";
Expected output:
error TS2322: Type 'string' is not assignable to type 'number'.
let count = 5 behaves exactly as if you had written let count: number = 5 — the annotation adds nothing. const goes one step further and keeps the exact literal, because the value can never change:
let mode = "light"; // string — may be reassigned to any string
const limit = 10; // 10 — only ever the value 10
Literal types look fussy now; they become genuinely useful when you start building state machines from "loading" | "error" values.
When an annotation really is required
Inference works wherever there is a value to read. That leaves a short list of places where you must speak up:
- Function parameters — nothing infers them at declaration time, so under
strictan untyped parameter is an error. You will meet this properly in Functions in TypeScript. - Declarations without an initializer —
let score: number;has nothing to infer from; using it before assigning triggerserror TS2454: Variable 'score' is used before being assigned. - Empty collections —
let ids: number[] = [];tells the compiler what may go in before the first element arrives. - Incoming or untrusted data —
JSON.parseand friends know nothing about your shapes.
One annotation per boundary, none per line. A useful rule of thumb: annotate the inputs and outputs of functions — the contract — and let inference handle everything inside:
function totalPrice(price: number, quantity: number): number {
return price * quantity;
}
const items = totalPrice(9.99, 3); // inferred as number
Expected output: console.log(items) prints 29.97. The call site needed no annotation at all, yet hovering over items shows number.
unknown: the safe version of I don't know
Sometimes you genuinely have no idea what a value is. any answers that by switching the compiler off; unknown answers it by making the compiler wait until you prove what you have:
let input: unknown = JSON.parse('{"score": 42}');
console.log(input.score);
Expected output:
error TS18046: Variable 'input' is of type 'unknown'.
Nothing is allowed on an unknown value until it has been narrowed. Check first, then convert:
if (typeof input === "object" & input !== null & "score" in input) {
const score = (input as { score: number }).score;
console.log(score);
}
Expected output: 42. The cast is safe here only because the guard ran immediately above it.
| Question | any | unknown |
|---|---|---|
| Can anything be assigned to it? | Yes | Yes |
| Can you use it without checking? | Yes — that is the danger | No — you must narrow first |
| What does it feel like? | Dynamic typing | A sealed box with a label |
Reading type errors
Every compiler message follows the same shape — location, code, explanation — and the second half is almost always the whole story:
src/main.ts(3,8): error TS2322: Type 'string' is not assignable to type 'number'.
src/main.ts(5,7): error TS2304: Cannot find name 'pricee'.
Read them in this order: the TS number identifies the family of mistake, the message states the two types involved, and the location sends your cursor to the line. If the location points at the line after your mistake, do not panic — an unclosed brace or a missing return often reports itself downstream. And when a message names a type you do not recognise, note that it is quoted exactly as the compiler sees it: the best clue you will get.
Common mistakes
- Annotating every single variable —
let count: number = 5everywhere adds noise without adding safety; inference already knows. - Looking for an
inttype — TypeScript has onlynumber;age = 36.5compiles happily, so validate ranges yourself. - Assigning
nullwithout declaring it — under strict mode writestring | nullor use an optional property instead of fighting TS2322. - Reaching for
anywhen a value is unknown —anydisables checking;unknownpauses it until you narrow. - Trusting an annotation to validate data —
const u: User = JSON.parse(x)checks nothing at runtime; the types are erased after compilation.
Practice checklist
- Declare one variable of each primitive type above, reassign each to a wrong type, and collect the five error messages.
- Write
let total = 10with and without an annotation, confirm the inferred type by hovering, and trigger the same TS2322 both ways. - Try
let n: number = null, read the error, then fix it withnumber | null. - Store a parsed JSON value in an
unknown, reproduce TS18046, then narrow it and log a property successfully. - Type a function with two annotated parameters and one unannotated parameter; note which line the compiler complains about.
What's next?
Primitives are only the first layer — next you will type groups of values in Arrays, Tuples and Objects.
Frequently Asked Questions
TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.
You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.
No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.
If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.
Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.
strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.
any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.
Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.
Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.
Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.