TypeScript Types and Type Inference: A Practical Introduction
ProgrammingLesson 1 of 45 min read
What you'll learn
- Explain what TypeScript adds to JavaScript and what it does not do at runtime
- Annotate variables and function signatures, and know when to let inference do the work
- Use strict null checking to handle
undefinedandnullexplicitly - Choose
unknownoveranyand read the most common compiler errors
On this page
A bike-share app has a function that prints a status line for each station: describe(station, bikes). It has run for months. Then one night a station goes offline, the feed stops sending the bike count, the function receives undefined, calls .toFixed() on it, and the dashboard page crashes. In JavaScript nothing warned anyone because nothing could. TypeScript's whole value proposition is that this class of mistake, a value being something other than you assumed, is reported while you type instead of at 3 a.m.
What TypeScript is, and what it is not
TypeScript is JavaScript plus a static type system. You write .ts files, the compiler (tsc) checks them, and the types are then erased: the output is plain JavaScript with no trace of them. That has two consequences worth memorising. First, types cost nothing at runtime. Second, types also protect nothing at runtime: if data arrives from a network call or a form, TypeScript cannot verify it is the shape you claimed. We return to that in the last lesson.
Turn on strict mode ("strict": true in tsconfig.json) from day one. Without it, TypeScript quietly allows null anywhere and treats missing annotations as any, which removes most of the benefit. Everything in this course assumes strict mode.
Annotations and inference working together
You can annotate a value with a colon and a type: let freeDocks: number = 12. Usually you should not, because TypeScript infers the type from the initial value. Annotate where inference cannot reach: function parameters, return types of exported functions, and variables declared without a value. Notice also that const infers the narrowest possible type: const stationName = "Riverside" has the literal type "Riverside", whereas let widens to string, since a let may be reassigned.
// docks.ts - a bike-share station
const stationName = "Riverside"; // inferred: "Riverside" (a literal type)
let freeDocks = 12; // inferred: number
let isOpen = true; // inferred: boolean
freeDocks = freeDocks - 1;
const bikeIds: number[] = [301, 302, 305];
const lastCheck: [string, number] = ["08:15", 4]; // tuple: fixed length and order
function docksLeft(total: number, bikes: number): number {
return total - bikes;
}
function describe(station: string, bikes?: number): string {
return bikes === undefined ? `${station}: unknown` : `${station}: ${bikes} bikes`;
}
const logLine = (msg: string): void => console.log(msg);
console.log(docksLeft(20, bikeIds.length));
logLine(describe(stationName));
logLine(describe(stationName, bikeIds.length));
console.log(lastCheck[0], isOpen);Output
17 Riverside: unknown Riverside: 3 bikes 08:15 true
This file contains only type syntax, so you can run it directly on recent Node versions with node --experimental-strip-types docks.ts (the flag is needed on some Node 22 releases) or compile it with npx tsc. The bikes?: number parameter is optional and has the type number | undefined, and the function handles that case explicitly, which is precisely the bug from the opening story, now fixed by design. void marks a function that returns nothing useful.
Letting the compiler tell you what is wrong
The next file is wrong on purpose. It will not compile, and each comment shows the real TypeScript 5.9 error for the line beside it. Reading these messages is a skill: they are long but they always say what the compiler expected and what it found.
let freeDocks = 12;
freeDocks = "none"; // (1)
function describe(station: string, bikes?: number): string {
return `${station}: ${bikes.toFixed(0)} bikes`; // (2)
}
function findStation(id: number): string | undefined {
return id === 1 ? "Riverside" : undefined;
}
const name = findStation(2);
console.log(name.toUpperCase()); // (3)
function parse(raw: unknown) {
return raw.length; // (4)
}
const trip: [string, number] = ["08:15", 4, 9]; // (5)Output
(1) Type 'string' is not assignable to type 'number'.
(2) 'bikes' is possibly 'undefined'.
(3) 'name' is possibly 'undefined'.
(4) 'raw' is of type 'unknown'.
(5) Type '[string, number, number]' is not assignable to type '[string, number]'.
Source has 3 element(s) but target allows only 2.Errors (2) and (3) come from strict null checking: a value typed string | undefined cannot be used as a string until you have handled the undefined case. Error (4) shows why unknown is the safe way to say "I do not know what this is". You must check it before using it. Its dangerous cousin any switches the checker off for that value, so anything.toFixed() compiles even when anything is a string and then throws at runtime. Treat every any as a debt you are choosing to take on.
| Type | Means | Use it when |
|---|---|---|
unknown | A value of any type that you must check before use | Parsed JSON, catch variables, anything from outside |
any | Opt out of checking for this value | Rarely: a stop-gap while migrating old JavaScript |
never | A value that cannot exist | Exhaustiveness checks and functions that always throw |
void | A function returns nothing useful | Callbacks and logging helpers |
T | undefined | Maybe a T, maybe nothing | Optional parameters and lookups that can miss |
Common mistakes and the errors they produce
- Leaving strict mode off. Nothing complains about
null, so the runtime errorTypeError: Cannot read properties of undefinedstill reaches production. Enablestrict. - Parameters without types. Under
noImplicitAny(part ofstrict),function f(x) {}givesParameter 'x' implicitly has an 'any' type.Annotate it. - Silencing errors with `any` or `as`. The red squiggle disappears but the crash remains. Fix the model, or use
unknownand narrow. - Annotating everything.
const count: number = 0adds noise and no safety. Annotate where inference cannot reach. - Believing types validate data.
const trip = JSON.parse(text) as Tripcompiles whatever the text contains. Validate data at runtime when it crosses a trust boundary. - Expecting `let x = "a"` to be a literal. It widens to
string, solet mode = "open"cannot be passed where"open" | "closed"is required. Useconst, an annotation, oras const.
How this shows up in real projects and interviews
Most teams adopt TypeScript for editor support as much as for error catching: autocomplete that knows what is in an object, safe renames across a codebase, and function signatures that document themselves. Reviewers expect strict on and are suspicious of any. In interviews, a common question is "what is the difference between any and unknown", and another is "are TypeScript types checked at runtime", which you can now answer with a clear no. Next you will learn to model real data shapes with interfaces, type aliases and unions, and if you build UIs, React pairs naturally with this.
Try it yourself
Write a function occupancyLabel(total: number, bikes?: number) that returns "unknown" when bikes is missing, "empty" when it is 0, "full" when it equals total, and "12 of 20"-style text otherwise. Then deliberately call it with a string argument and read the compiler error. Finally write parseCount(raw: unknown): number that returns the number if raw is a number, parses it if it is a numeric string, and throws otherwise.
Show a hint
Use typeof raw === "number" and typeof raw === "string" checks in parseCount, and Number.isNaN to reject strings that do not parse.
Key takeaways
- TypeScript checks types at compile time and then erases them; nothing is verified at runtime.
- Enable
strictmode, which turns on null checking andnoImplicitAny. - Annotate parameters and exported return types; let inference handle local variables.
constinfers literal types whileletwidens them.- Prefer
unknowntoany, and handleundefinedexplicitly instead of silencing the error.
Test yourself
A trick from this lesson's ride: Types are scaffolding: they come down before the building opens Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between any and unknown in TypeScript?
any turns off type checking for a value, so you can call anything on it and the compiler stays silent. unknown also accepts any value but forces you to check its type (for example with typeof) before you use it. Prefer unknown for data you do not control.
Does TypeScript check types at runtime?
No. The compiler checks types while you build, then removes them, so the JavaScript that runs has no type information. Data from APIs, forms or JSON.parse can still have the wrong shape, so validate it at runtime where it enters your program.
Should I add type annotations to every variable in TypeScript?
No. TypeScript infers the type from the value you assign, so annotations on simple local variables are noise. Add annotations to function parameters, to values declared without an initial value, and to exported functions where the explicit return type documents the contract.