A working TypeScript setup for a Node.js project needs three things: the TypeScript compiler (tsc) to turn .ts files into JavaScript for production, a way to run .ts files directly while you develop, and something that restarts the app when you save. The classic combination is typescript, ts-node and nodemon, all installed as dev dependencies, with a tsconfig.json and a few package.json scripts tying them together.
This guide builds that setup from an empty folder, explains the tsconfig.json options that matter, and fixes a common mistake in the start script. At the end it compares ts-node with newer options like tsx and Node's built-in type stripping, so you can choose what fits your project.
ECMAScript, JavaScript and TypeScript
These three names describe different things:
| Name | What it is |
|---|---|
| ECMAScript | The language specification, published yearly by TC39 (ES2015, ES2022, ES2025...). It defines syntax and built-ins. |
| JavaScript | The language as implemented by engines like V8 (Chrome, Node.js) and SpiderMonkey (Firefox), following ECMAScript. |
| TypeScript | A superset of JavaScript from Microsoft that adds static types. Every valid JavaScript file is valid TypeScript syntax. |
TypeScript never runs directly in the engine. The compiler checks your types, then removes them and emits plain JavaScript. The target option decides which ECMAScript version that output uses. Types exist only at development time, which is why TypeScript can't validate data arriving at runtime from an API; you still need runtime validation for that.
For more on what the type system gives you, see TypeScript basic types: any, unknown and never.
Prerequisites for the TypeScript setup
- Node.js, a current LTS version. Check with
node -v. - A package manager. The examples use Yarn. The npm equivalents are:
| Task | Yarn | npm |
|---|---|---|
Create package.json | yarn init -y | npm init -y |
| Add a dev dependency | yarn add -D typescript | npm install -D typescript |
| Run a local binary | yarn tsc | npx tsc |
| Run a script | yarn dev | npm run dev |
Step 1: initialize the project
mkdir my-service && cd my-service
yarn init -y
mkdir srcyarn init -y creates package.json with default values and skips the questions. On Yarn 2 and later, plain yarn init does the same.
Step 2: install TypeScript
yarn add -D typescript @types/nodeThe -D flag saves them as devDependencies. They're needed to build and develop, but the compiled JavaScript in production doesn't import them. Keeping them out of dependencies makes production installs smaller.
@types/node provides type definitions for Node's built-ins, such as process, Buffer and node:fs. Without it, process.env.PORT fails with "Cannot find name 'process'". The article on declaration files and @types packages explains how these packages work.
Installing TypeScript per project instead of globally means every developer and the CI server use the same compiler version, pinned in package.json.
Step 3: create and edit tsconfig.json
yarn tsc --initThis writes a tsconfig.json. Its contents depend on your TypeScript version: older 5.x releases write a long file with most options commented out, and newer ones write a shorter, stricter file aimed at modern modules. Either way, it's worth replacing it with a config you understand. For a Node.js service:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2023"],
"types": ["node"],
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true
},
"include": ["src"]
}The tsconfig options that matter
| Option | What it does | Why this value |
|---|---|---|
target | ECMAScript version of the emitted JavaScript | Current Node LTS versions support ES2022 syntax natively, so nothing needs downleveling |
module / moduleResolution | How imports are emitted and resolved | NodeNext follows Node's real rules for CommonJS and ES modules |
lib | Which built-in APIs the type checker knows about | Matches the runtime; no DOM types in a server project |
types | Which @types/* packages are loaded automatically | Only Node's. Add others, like jest, as you install them |
rootDir / outDir | Where source lives and where output goes | Keeps src/ and dist/ separate |
strict | Turns on all strict checks, including strictNullChecks and noImplicitAny | Most of TypeScript's value comes from these. Turn it on from day one |
esModuleInterop | Lets you import express from 'express' for CommonJS packages | Avoids import * as workarounds |
skipLibCheck | Skips type-checking .d.ts files in node_modules | Faster builds and fewer errors from third-party types |
sourceMap | Emits .js.map files | Stack traces and debuggers point at your .ts lines |
With module: "NodeNext", the output format follows package.json. Without a "type" field, files compile to CommonJS, which is the simplest setup to use with ts-node. If you add "type": "module", you get ES modules, and relative imports must include the .js extension (import { db } from './db.js').
Step 4: write code and compile it with tsc
// src/index.ts
import { createServer } from 'node:http';
const port = Number(process.env.PORT ?? 3000);
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, path: req.url }));
});
server.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});Compile and run the output:
yarn tsc
node dist/index.jstsc reads tsconfig.json, type-checks everything under src, and writes dist/index.js plus a source map. If there's a type error, it reports it and, by default, still emits output. Add "noEmitOnError": true if you want failed builds to produce nothing.
Step 5: run TypeScript directly with ts-node
Compiling before every run is slow during development. ts-node compiles in memory and runs the result in one step:
yarn add -D ts-node
yarn ts-node src/index.tsBy default ts-node type-checks every file it loads, which slows startup in larger projects. You can switch that off for development and leave type-checking to your editor and CI. Add a top-level ts-node section to tsconfig.json, next to compilerOptions:
"ts-node": {
"transpileOnly": true
}If you do this, add a typecheck script (shown below) and run it in CI, or type errors can reach the main branch unnoticed.
Step 6: restart on save with nodemon
nodemon watches files and restarts the process when they change:
yarn add -D nodemonConfigure it in nodemon.json at the project root:
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node ./src/index.ts"
}watch: only the source folder, notnode_modulesordist.ext: which file extensions trigger a restart.ignore: changes to test files shouldn't restart the server.exec: the command nodemon runs, here ts-node with the entry file.
Step 7: package.json scripts
{
"name": "my-service",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nodemon",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.0.0",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.0"
}
}Your installed versions will differ; that's fine. The important detail is start. Some setups write "start": "node src/index.js", which fails, because src/ contains .ts files. Production runs the compiled output, so start must point at dist/index.js, and build must run first.
The daily workflow:
yarn devwhile developing. Save a file and the server restarts.yarn typecheckbefore committing, and in CI.yarn build && yarn startin production or in your Docker image.
Add the build output to .gitignore:
node_modules/
dist/The project now looks like this:
my-service/
├── src/
│ └── index.ts
├── dist/ # generated by tsc
├── nodemon.json
├── package.json
└── tsconfig.jsonAlternatives to ts-node and nodemon
ts-node still works, but it's no longer the only choice, and its ES module support takes extra configuration. Two alternatives are common now:
| Tool | Type-checks | Watch mode | Notes |
|---|---|---|---|
tsc | Yes | tsc --watch (compile only) | Produces production output |
ts-node + nodemon | Yes, unless transpileOnly | Via nodemon | Easiest with CommonJS |
tsx | No | tsx watch src/index.ts | esbuild-based, fast, handles CommonJS and ES modules |
node with type stripping | No | node --watch src/index.ts | Built into Node 22.18+ and 23.6+. Supports only erasable syntax: no enum, namespace or constructor parameter properties |
With tsx, the dev script becomes one line and nodemon isn't needed:
{
"scripts": {
"dev": "tsx watch src/index.ts"
}
}None of the fast runners type-check, so whichever you pick, keep tsc --noEmit in CI. For new projects, tsx or native Node is usually simpler. For an existing ts-node setup that works, there's no urgent reason to migrate.
FAQ
Should TypeScript be a dependency or a devDependency?
A devDependency. It's only needed to compile. Production runs the JavaScript in dist/, which doesn't import TypeScript.
What is the difference between tsc and ts-node?
tsc compiles .ts files to .js files on disk. ts-node compiles in memory and runs the code immediately, without writing files. Use tsc for builds and ts-node (or tsx) for development.
Why do I get "Cannot find name 'process'" or "Cannot find name 'require'"?
The Node.js type definitions are missing. Install @types/node and make sure "types" in tsconfig.json includes "node", or leave types out so all installed @types packages load.
Do I need nodemon if I use tsx?
No. tsx watch restarts on file changes itself. Node's own --watch flag does the same when you run .ts files with type stripping.
Can I install TypeScript globally instead?
You can, but a per-project install pins the version in package.json, so every developer and CI server compiles with the same one. Run it with yarn tsc or npx tsc.
Setup checklist
-
yarn init -yand asrc/folder -
typescriptand@types/nodeas devDependencies - A
tsconfig.jsonwithstrict,rootDir: "src"andoutDir: "dist" -
ts-nodeandnodemon(ortsx) for development - Scripts:
dev,build,startpointing atdist/, andtypecheck -
dist/andnode_modules/in.gitignore
With this in place, the next step is using the type system well, starting with interfaces vs. type aliases. If you're planning a larger Node.js or TypeScript system and want experienced help, Vectorkub builds custom software and web apps.
