Setting Up TypeScript with Node.js
TypeScript adds static types to JavaScript, giving you compile-time checking, richer editor autocompletion, and safer refactoring while still emitting plain JavaScript that Node runs. Setting it up for a Node.js project is mostly a matter of installing the compiler, adding the Node type definitions, and writing a tsconfig.json tuned for your runtime. This page walks through a clean, modern setup on Node 20/22 LTS using ES modules, including the key compiler options and the build-and-run workflow.
Installing TypeScript and the Node types
TypeScript and its type definitions belong in devDependencies — they are build-time tools, not runtime code. Start a project and install three things: the typescript compiler, @types/node (which describes the shape of Node’s built-in APIs like node:fs and process), and tsx for a fast dev-time runner.
mkdir ts-app && cd ts-app
npm init -y
npm install --save-dev typescript @types/node tsx
Pin @types/node to your runtime’s major version so the types match the APIs actually available. On Node 22, for example, install @types/node@22.
npm install --save-dev @types/node@22
Verify the compiler is available through npx:
npx tsc --version
Output:
Version 5.8.3
Initializing tsconfig.json
The compiler reads its settings from tsconfig.json at the project root. Generate a documented starter file with the --init flag, then trim it down to what a Node project needs.
npx tsc --init
Replace the generated file with a lean configuration suited to modern Node and ES modules:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true,
"declaration": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Because we use ES modules, set "type": "module" in package.json so the emitted .js files are treated as ESM by Node:
{
"name": "ts-app",
"version": "1.0.0",
"type": "module"
}
With
"module": "NodeNext", relative imports in your TypeScript source must include the.jsextension —import { greet } from "./greet.js"— even though the file on disk isgreet.ts. The extension refers to the compiled output. This trips up almost everyone the first time.
Key compiler options
These are the options that most affect a Node project’s behaviour and safety.
| Option | Recommended value | Why it matters |
|---|---|---|
target | ES2022 | Output syntax level; Node 20/22 support ES2022 natively, no down-leveling. |
module | NodeNext | Emits ESM/CJS following Node’s resolution rules. |
moduleResolution | NodeNext | Resolves imports the way Node does, honouring package.json exports. |
strict | true | Turns on all strict type checks (null checks, implicit any, etc.). |
outDir / rootDir | ./dist / ./src | Keeps generated .js separate from source .ts. |
esModuleInterop | true | Smooths importing CommonJS packages with default imports. |
sourceMap | true | Maps stack traces back to TypeScript lines when debugging. |
declaration | true | Emits .d.ts files — useful when publishing a library. |
skipLibCheck is worth keeping on: it skips type-checking inside node_modules, which speeds up builds and avoids errors caused by conflicting third-party type versions.
Writing and building code
Put source files under src/. Here is a small program that uses a typed function and a native Node core module:
// src/greet.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
// src/index.ts
import { hostname } from "node:os";
import { greet } from "./greet.js";
const message: string = greet(hostname());
console.log(message);
Add scripts to package.json for building and running the compiled output:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
}
Compile the project, then run the emitted JavaScript:
npm run build
npm start
Output:
Hello, my-laptop!
The typecheck script runs the compiler with --noEmit, validating types without writing any files — ideal for CI pipelines and pre-commit hooks.
Running without a build step
During development, recompiling on every change is tedious. The tsx runner executes TypeScript directly and supports watch mode, so you get a fast inner loop without a manual tsc build:
npx tsx watch src/index.ts
This is for development only. For production you should still run tsc and ship the compiled JavaScript, which starts faster and removes the dev toolchain from your runtime. Newer Node versions can also strip types natively, but a full tsc build remains the most portable choice.
Best Practices
- Keep
typescriptand@types/nodeindevDependencies, and match@types/nodeto your Node major version. - Always enable
strict: trueon new projects — it catches whole classes of bugs at compile time. - Separate source and output with
rootDir/outDir, and adddist/to.gitignore. - With
NodeNext, write relative imports with the.jsextension to match the compiled output. - Use
tsxfor the dev loop, but build withtscand run plain Node in production. - Add a
tsc --noEmittypecheck step to CI so type errors fail the build before deploy.