Skip to content

02 β€” Your Toolchain β€” Node, npm, VS Code and the Compiler

Level: Beginner Β· Time: ~30 min Β· Prerequisites: Lesson 1 β€” What TypeScript Is, and Why It Exists


Why this matters

The compiler Lesson 1 promised is an ordinary program you install and configure, and the configuration is where beginners lose evenings. A project whose tsconfig.json disagrees with its package.json, or whose compiler came from a global install nobody remembers, produces errors that look like language problems but are toolchain problems. Four programs do the work: Node runs JavaScript, npm installs and records packages, the editor's language server checks types as you type, and tsc checks them from a terminal. Install them once, deliberately, and every remaining lesson is only about TypeScript.

The TypeScript toolchain: Node.js runtime, npm, the tsc compiler, VS Code, tsconfig.json and the project layout

Click the diagram to open it full size.

The mental model: four programs, one pipeline

TypeScript has no runtime, so something else always executes your code.

Program Runs on Its one job
Node.js your machine executes plain JavaScript β€” the only thing that ever runs
npm Node downloads packages; writes package.json and package-lock.json
tsc Node reads .ts, checks types, emits .js into dist/
TS language server Node, inside the editor the same checker in editor mode: squiggles, hover, autocomplete

The pipeline for every file you write: edit .ts β†’ tsc checks β†’ tsc erases the types and emits .js β†’ node runs the .js.

Installing Node with a version manager

Install through fnm or nvm, never apt install nodejs: the system package manager writes into root-owned directories, which is the origin of the EACCES: permission denied errors that push people into sudo npm install -g. A version manager keeps Node in your home directory and lets each project pin its own version.

fnm install 22 && fnm use 22 && fnm default 22   # or: nvm install 22 && nvm use 22
node -v            # v22.x.y β€” anything 20 or newer is fine
npm -v             # 10.x.y
node -v > .nvmrc   # optional: lets `fnm use` / `nvm use` pick the right version

Node 20 is the minimum because it is the first long-lived LTS line that also ships modern module resolution and fetch globally. Everything here assumes Node 20+ and ESM; on Node 16 you will meet errors that have nothing to do with types.

VS Code: the language server is the point

TypeScript support is built in β€” there is no extension to install. Four settings are worth doing on day one: the Problems panel (Ctrl+Shift+M, Cmd+Shift+M on macOS) lists every error with file and line; quick fixes (Ctrl+.) offer the compiler's own suggestions; "editor.formatOnSave": true keeps diffs readable; and "typescript.tsdk": "node_modules/typescript/lib", followed by the command palette entry TypeScript: Select TypeScript Version β†’ Use Workspace Version, makes the editor use the compiler you actually installed rather than a bundled one.

[!IMPORTANT] If the editor is happy and npm run typecheck is not, the two are reading different tsconfig.json files. The status bar of an open .ts file shows which config the language server chose.

Local, not global: npx and the runners

Tools a project needs belong in that project's devDependencies, so a colleague gets identical versions from one npm install:

mkdir -p ~/dev/ts-zero-to-hero && cd ~/dev/ts-zero-to-hero
npm init -y
npm install --save-dev typescript @types/node tsx
npx tsc --version        # Version 5.x.y

npx <tool> looks in ./node_modules/.bin first and only reaches the network if it finds nothing, which is why npx tsc needs no global install. @types/node supplies the types for process, node:fs and the rest β€” without it the compiler does not know process.env exists.

Command What it does When it is right
npx tsc checks, then emits JavaScript into dist/ npm run build, and anything you deploy
npx tsx file.ts strips the types in memory and runs the file experiments and one-off scripts
npx tsx watch file.ts runs, then re-runs on every save the everyday development loop
node dist/index.js runs emitted JavaScript only production, and debugging built output

nodemon is still widely used, but it was built for JavaScript: watching TypeScript means configuring it to run tsx as its executor. tsx watch does the same job with no configuration at all.

[!WARNING] tsx erases types; it does not check them. A file with a real type error runs happily under tsx and only fails when tsc runs β€” which is why npm run typecheck exists.

tsconfig.json, read line by line

Replace whatever npx tsc --init generates with this, the configuration the course assumes:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"]
}
  • target β€” which JavaScript version is emitted. Node 20 understands ES2022 natively, so nothing is down-levelled and the output resembles the source.
  • lib β€” which global APIs the checker believes exist. Leaving DOM out is deliberate: in a Node project a stray document.querySelector should be a compile error, not a runtime mystery.
  • module / moduleResolution β€” emit and resolve modules the way Node 20 really does, which means ESM. Because resolution is real, relative imports carry the .js extension even though the file you wrote is .ts: import { total } from "./yield.js". Lesson 12 covers this properly.
  • rootDir / outDir β€” the input and output trees; dist/ mirrors src/ exactly, so src/report.ts becomes dist/report.js.
  • strict β€” a bundle of checks against null, unannotated parameters and several unsound patterns. It is one flag because it is meant to be on; Lesson 3 unpacks the parts.
  • skipLibCheck β€” does not check your dependencies' .d.ts files. Real build speed, at the price of hiding errors inside libraries you cannot fix anyway.
  • sourceMap β€” emits .js.map files so a debugger steps through your original .ts (Lesson 3).

Layout, scripts and what not to commit

The layout is src/ for your TypeScript, dist/ for generated output, plus package.json, package-lock.json and .gitignore β€” nothing else at the top level.

package.json needs "type": "module" and four scripts, which become the vocabulary of the course:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts"
  }
}

typecheck runs the compiler with --noEmit: it reports errors and writes nothing, which is the check you will run a hundred times a day. .gitignore holds node_modules/, dist/, *.log and .env β€” both generated trees are reproducible from package.json and src/, so they are noise in a diff. package-lock.json is the opposite: commit it, because it is what makes an install identical on another machine six months from now.


Common mistakes

Mistake What happens The fix
npm install -g typescript A second, silently different compiler the project does not control Install locally with --save-dev and call npx tsc
Committing dist/ or node_modules/ Diffs nobody can review, plus stale output hiding build failures Ignore both; commit package.json and the lock file
import { total } from "./yield" under NodeNext Node throws ERR_MODULE_NOT_FOUND when the built code runs Write the extension the runtime sees: "./yield.js"
Trusting tsx watch as a type check Type errors reach your commit because a run succeeded Run npm run typecheck before every commit
Editing files inside dist/ The next npm run build overwrites your change silently Edit only src/; treat dist/ as disposable
Leaving lib at its default in a Node project document and window type-check, then crash at runtime Set "lib": ["ES2022"] and install @types/node

Try it

  1. Build the skeleton and confirm the tools are local:
cd ~/dev/ts-zero-to-hero && mkdir -p src
npm install --save-dev typescript @types/node tsx
npx tsc --version

Expected: a line such as Version 5.6.3, and ls node_modules/.bin listing tsc and tsx.

  1. Create src/index.ts with one line: console.log("solar tracker: bootstrap complete");

  2. Run the check, the build and the output in order:

npm run typecheck && npm run build && ls dist && npm start

Expected: typecheck prints nothing β€” silence is success. ls dist shows index.js and index.js.map. npm start prints solar tracker: bootstrap complete.

  1. Prove the two runners differ. Replace the line with const annualYieldKwh: number = "five thousand"; console.log(annualYieldKwh); and compare:
npx tsx src/index.ts   # prints "five thousand" β€” no complaint at all
npm run typecheck      # error TS2322: Type 'string' is not assignable to type 'number'.

Delete the broken line afterwards. This contrast is the single most useful thing to carry out of the lesson.


Key takeaways

  • tsc checks, erases the types and emits JavaScript; Node always runs JavaScript and never runs TypeScript.
  • Install Node with a version manager and the compiler as a local devDependency; global installs cause permission errors and version drift.
  • tsx runs TypeScript but checks nothing, so npm run typecheck (tsc --noEmit) belongs in every project you create.
  • tsconfig.json is the contract between source and compiler: target and lib decide which JavaScript exists, module and moduleResolution decide how imports work, rootDir and outDir decide where output goes.
  • src/ is yours; dist/ and node_modules/ are generated and ignored; package-lock.json is committed so installs are reproducible.

Check yourself

  1. A colleague's editor shows no errors while npm run typecheck fails on the same commit. Name two plausible causes and how you would confirm each.
  2. Why does this course keep DOM out of lib for a Node program β€” what class of mistake does that turn into a compile error?
  3. You move @types/node from devDependencies to dependencies. What changes, and when?
  4. Explain why npm start runs dist/index.js rather than src/index.ts, and what would have to be true for that to be wrong.

Next

Lesson 3 β€” Your First TypeScript Program