Skip to content
Node.js nd typescript 4 min read

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 .js extension — import { greet } from "./greet.js" — even though the file on disk is greet.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.

OptionRecommended valueWhy it matters
targetES2022Output syntax level; Node 20/22 support ES2022 natively, no down-leveling.
moduleNodeNextEmits ESM/CJS following Node’s resolution rules.
moduleResolutionNodeNextResolves imports the way Node does, honouring package.json exports.
stricttrueTurns on all strict type checks (null checks, implicit any, etc.).
outDir / rootDir./dist / ./srcKeeps generated .js separate from source .ts.
esModuleInteroptrueSmooths importing CommonJS packages with default imports.
sourceMaptrueMaps stack traces back to TypeScript lines when debugging.
declarationtrueEmits .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 typescript and @types/node in devDependencies, and match @types/node to your Node major version.
  • Always enable strict: true on new projects — it catches whole classes of bugs at compile time.
  • Separate source and output with rootDir/outDir, and add dist/ to .gitignore.
  • With NodeNext, write relative imports with the .js extension to match the compiled output.
  • Use tsx for the dev loop, but build with tsc and run plain Node in production.
  • Add a tsc --noEmit typecheck step to CI so type errors fail the build before deploy.
Last updated June 14, 2026
Was this helpful?