Skip to content

03 β€” Your First TypeScript Program

Level: Beginner Β· Time: ~25 min Β· Prerequisites: Lesson 2 β€” Your Toolchain β€” Node, npm, VS Code and the Compiler


Why this matters

Everything from here on is one loop repeated: edit a file, let the compiler object, fix it, run it. Learning to read a diagnostic properly β€” file, position, code, the what and the why β€” turns a red squiggle from an obstacle into the fastest bug detector you will own. The second distinction this lesson makes concrete separates engineers from people who merely use TypeScript: a compile error and a runtime crash are different failures, because the compiler knows your code and nothing at all about your data.

The loop: two ways from source to output

Path Commands What happens to your types
Compile, then run npm run build && node dist/index.js Checked, then erased β€” the output is plain JavaScript
Run directly npx tsx src/index.ts Erased only. No checking at all

Develop with npx tsx watch src/index.ts, but treat npm run typecheck as the authority. When both agree, npm run build produces the artefact you would ship.

Building the tracker, end to end

Three days of yield β€” the numbers you would read off an inverter or download from PVGIS.

cd ~/dev/ts-zero-to-hero && mkdir -p src

src/index.ts:

type DailyYield = { readonly day: string; readonly kwh: number };

const yields: readonly DailyYield[] = [
  { day: "2026-03-01", kwh: 18.4 },
  { day: "2026-03-02", kwh: 21.9 },
  { day: "2026-03-03", kwh: 12.6 },
];

const totalKwh = yields.reduce((sum, entry) => sum + entry.kwh, 0);
const averageKwh = totalKwh / yields.length;

console.log(`Three-day total: ${totalKwh.toFixed(1)} kWh`);
console.log(`Daily average:   ${averageKwh.toFixed(1)} kWh`);

Notice what is missing: no annotation on sum, none on entry, none on totalKwh β€” the compiler infers all of them from yields. That is the habit this course teaches: annotate the data shapes, infer the arithmetic.

npm run typecheck        # silence means success
npm run build
node dist/index.js

npx tsx src/index.ts     # the same program, without an output folder

Expected output, from both runs:

Three-day total: 52.9 kWh
Daily average:   17.6 kWh

Reading a compiler error like a professional

Break the program deliberately: change the first reading from 18.4 to "18.4" β€” a string, exactly what a CSV column or a JSON field hands you β€” and run npm run typecheck:

src/index.ts(4,24): error TS2322: Type 'string' is not assignable to type 'number'.

Five pieces of information, in the order you should read them:

  1. src/index.ts β€” the file. With dozens of files, this alone saves minutes.
  2. (4,24) β€” line 4, column 24: the exact position of the offending expression (the "18.4" you just typed).
  3. TS2322 β€” the diagnostic code, stable and searchable, and what the editor offers a quick fix for.
  4. Type 'string' is not assignable to type 'number' β€” the what: what you provided, and what was required. Read it left to right.
  5. The why is underneath β€” a follow-up line, or the hover text naming the declaration that demanded a number. Here it is DailyYield.kwh.

Two more messages you will meet within the first hour: error TS2345: Argument of type 'string' is not assignable to parameter of type 'readonly DailyYield[]'., which means the call site is wrong; and error TS7006: Parameter 'entry' implicitly has an 'any' type., which means the compiler could not infer a parameter and gave up β€” that function body is now unchecked, which is worse than an error.

[!TIP] Fix one error, then re-run the check. A single wrong type can produce thirty downstream diagnostics, and chasing them in parallel is how people edit code that was never broken.

What strict: true actually turns on

strict is not one check but a bundle, and each member closes a class of bug that JavaScript ships by default.

Flag inside strict The bug it stops
strictNullChecks Reading null or undefined as if it were a value β€” the commonest crash in JavaScript
noImplicitAny A parameter the compiler could not type, silently unchecked for the whole body
strictFunctionTypes Callbacks whose parameters are accepted unsoundly, so a string reaches a number
strictPropertyInitialization A class field used before anything assigned it
useUnknownInCatchVariables Assuming a caught error is an Error, when anything can be thrown

strictNullChecks is the one that changes how code looks: null becomes a real type, and a value that might be missing must be handled before use.

function kwhPerDay(totalKwh: number, days: number): number {
  if (days === 0) return 0;          // the guard the compiler forced you to write
  return totalKwh / days;
}

const maybeDays: number | undefined = undefined;
kwhPerDay(52.9, maybeDays ?? 3);     // βœ… the union was handled explicitly
// kwhPerDay(52.9, maybeDays);       ❌ TS2345: 'number | undefined' is not assignable to 'number'

A compile error is not a runtime crash

A compile error means the compiler read your source and refused to emit β€” nothing ran. A runtime crash means the program started, ran, and then threw: the expensive failure, and the one the compiler cannot always prevent, because types describe code, not data.

// This compiles cleanly. It crashes.
type Reading = { readonly region: string; readonly kwh: number };

const raw = JSON.parse('{"region": "Dakar"}') as Reading;
console.log(raw.kwh.toFixed(1));
// TypeError: Cannot read properties of undefined (reading 'toFixed')

The as Reading is a promise you made to the compiler and the JSON did not keep. Data from a file, a network response, an environment variable or a user must be validated at runtime; Lesson 23 shows how.

The mirror image is code the compiler rejects that would otherwise have crashed: days[5] on a three-element array is undefined, and crashing one call later β€” with noUncheckedIndexedAccess the checker stops you at the index instead.

Debugging, and where annotations belong

"sourceMap": true from Lesson 2 makes dist/index.js.map link the emitted JavaScript back to src/index.ts. With that in place, .vscode/launch.json gives you breakpoints in your own source:

{
  "version": "0.2.0",
  "configurations": [
    { "type": "node", "request": "launch", "name": "Debug built program",
      "program": "${workspaceFolder}/dist/index.js",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"], "sourceMaps": true }
  ]
}

Build first (npm run build), then press F5. Outside the editor, node --inspect dist/index.js prints an address a Chromium browser's chrome://inspect page can attach to, and node --inspect-brk stops before the first line β€” what you want when the program dies during start-up.

Where to annotate. The rule is: annotate the boundaries, infer the interior. Type the function parameters, the return types and the shapes of data entering the program; let the compiler infer everything inside a body and every value derived from something already typed. Inference is never stale; a hand-written annotation is. While debugging, log the value (yields) rather than a description of it ("checking yields").


Common mistakes

Mistake What happens The fix
Treating tsx output as proof the code is correct Type errors survive, because tsx never checks Run npm run typecheck as the gate before every commit
Fixing all errors at once Half of them vanish when the first is fixed, and you change code that was fine Fix the first error, re-run, repeat
Reaching for as to silence a diagnostic It compiles and the crash becomes invisible β€” the worst trade in TypeScript Handle the type the compiler found: a guard, a default, a union
Assuming JSON.parse results are typed any flows through unchecked and fails at the worst moment Treat the result as unknown and validate it (Lesson 23)
Debugging built output without source maps Breakpoints land in the wrong place and line numbers lie Keep sourceMap: true and outFiles pointing at dist/**/*.js
Adding ! to a possibly-undefined value to move on The crash happens later, further from its cause Guard it now: days[0] ?? "", or return early

Try it

  1. Add a function above the console.log lines, with explicit boundary types:
function highestDay(entries: readonly DailyYield[]): DailyYield | undefined {
  let best: DailyYield | undefined = undefined;
  for (const entry of entries) if (best === undefined || entry.kwh > best.kwh) best = entry;
  return best;
}

const best = highestDay(yields);
console.log(`Best day: ${best?.day ?? "no data"} (${best?.kwh.toFixed(1) ?? "0.0"} kWh)`);

The | undefined is not decoration: an empty array has no best day, and strictNullChecks forces you to say so.

  1. Run the loop and confirm the fourth line:
npm run typecheck && npx tsx src/index.ts

Expected:

Three-day total: 52.9 kWh
Daily average:   17.6 kWh
Best day: 2026-03-02 (21.9 kWh)
  1. Break it three ways, one at a time, recording the code and wording of each message before fixing it: change 18.4 to "18.4" (expect TS2322); call highestDay(52.9) (expect TS2345); replace the last line with console.log(best.kwh) (expect a "possibly 'undefined'" error).

  2. Set a breakpoint on the if (best === undefined ...) line, run npm run build, press F5. Expected: the debugger stops inside src/index.ts, not in dist/index.js β€” the source map doing its work.


Key takeaways

  • The compiler knows your code, not your data. A clean npm run typecheck says nothing about the JSON or CSV your program will read at runtime.
  • Every diagnostic carries five things, read in order: file, position, code, the what, the why.
  • strict: true is a bundle, and strictNullChecks and noImplicitAny account for most of the bugs it prevents.
  • Compile errors are cheap because nothing ran; runtime crashes are expensive because something did.
  • Annotate boundaries and infer the interior β€” TypeScript is not more annotated code, it is better-placed annotations.

Check yourself

  1. tsx printed a number that tsc rejects. What did each tool do, and which one told the truth about the program?
  2. Why does highestDay return DailyYield | undefined rather than DailyYield? What would the second version cost you at a call site written a month later?
  3. const raw = JSON.parse(text) as Reading compiles and crashes. Where exactly is the lie, and which technique removes it?
  4. You have a working tsx watch session and a failing npm run typecheck. Which do you believe, and what would you change about your workflow so the question cannot arise?

Next

Lesson 4 β€” Primitive Types and Type Inference