TypeScript in Node: The Practical Setup
You already write TypeScript for the browser. Now you need it running server-side—for an API, a build script, or SSR. The problem: most setup guides are outdated, recommending CommonJS configurations or tools that don’t align with modern Node.js.
This guide covers two approaches for your TypeScript Node.js setup: compiling with tsc and running JavaScript, or running .ts files directly with Node’s native type-stripping. Both work. Each fits different scenarios.
Key Takeaways
- Set
"type": "module"in package.json to enable ESM by default for modern TypeScript Node.js projects - Use
tsccompilation for production deployments, published packages, and code using enums, namespaces, or parameter properties - Use Node’s native type-stripping for local scripts, development servers, and quick prototypes
- Always use
import typefor type-only imports to avoid runtime errors with type-stripping - Run
tsc --noEmitin CI since Node’s type-stripping doesn’t perform type checking
The Baseline: Node 24 LTS and ESM
Start with this foundation:
{
"type": "module"
}
This enables ESM by default. Your imports use ESM syntax, and Node resolves modules accordingly.
Node 24 is the current LTS baseline for this setup (can be downloaded from here: https://nodejs.org/en/download).
Approach 1: Compile with tsc, Run JavaScript
This approach separates compilation from execution. Use it for production deployments, published packages, or when you need full TypeScript feature support.
tsconfig for Node 24
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"lib": ["ES2024"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Key settings in this configuration:
module: NodeNextandmoduleResolution: NodeNext: Matches Node’s actual module resolution behaviorverbatimModuleSyntax: Requires explicitimport typefor type-only imports—critical for avoiding runtime errors (see TypeScript docs: https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax)isolatedModules: Ensures compatibility with single-file transpilation tools
Scripts
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch"
}
}
Run npm run build, then npm start. The compiled JavaScript lives in dist/.
Approach 2: Node’s Native TypeScript (Type-Stripping)
Node introduced native TypeScript support in Node 22 and stabilized it in Node 24 LTS via type-stripping. Use it for scripts, local tooling, or development when you want zero build steps.
Official docs: https://nodejs.org/api/typescript.html
How It Works
In Node 24+, run directly:
node src/index.ts
(Older Node versions required experimental flags; Node 24 does not.)
Critical Limitations
Node’s native TypeScript strips types only—it doesn’t type-check. You still need tsc --noEmit in CI or your editor for catching errors.
Other constraints:
- Ignores tsconfig.json: Node doesn’t read your compiler options
- Requires explicit file extensions: Write
import { foo } from './utils.js'even when the source file isutils.ts - Respects ESM vs CJS rules: Your
package.jsontype field matters - Won’t run TypeScript from node_modules: Dependencies must be compiled JavaScript
- Only supports erasable syntax: Enums, namespaces, and parameter properties fail unless you enable
--experimental-transform-types
File Extensions Matter
For mixed module formats:
.mtsfiles → always ESM.ctsfiles → always CommonJS.tsfiles → followpackage.jsontype field
Discover how at OpenReplay.com.
Avoiding Runtime Errors
Use import type for type-only imports:
// Correct
import type { Request, Response } from 'express'
import express from 'express'
// Wrong - will fail at runtime with type-stripping
import { Request, Response } from 'express'
Enable verbatimModuleSyntax in your tsconfig to catch these during development, even though Node ignores the config at runtime.
Which Approach to Use
Use tsc compilation for:
- Production deployments
- Published npm packages
- Code using enums, namespaces, or parameter properties
- Projects requiring source maps in production
Use native type-stripping for:
- Local scripts and tooling
- Development servers (pair with
--watch) - Quick prototypes
- SSR development builds
A Practical Development Setup
Combine both approaches:
{
"scripts": {
"dev": "node --watch src/index.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"start": "node dist/index.js"
}
}
Development uses native execution for speed. CI runs typecheck. Production deploys compiled JavaScript.
Conclusion
Modern TypeScript Node.js setup is simpler than older guides suggest. Use ESM, configure NodeNext module resolution, and choose your execution strategy based on context. Native type-stripping works for development and scripts. Compiled output works for production and packages. Both approaches share the same source code and tsconfig—you’re not locked into either.
FAQs
Node's module resolution requires explicit file extensions for ESM. When you write import from ./utils.js, Node looks for that exact path at runtime, even if your source file is utils.ts. Since type-stripping removes types but doesn't rename files, and tsc outputs .js files, using .js extensions in your source ensures imports work in both scenarios.
Not by default. Enums require code transformation, not just type removal. You can enable --experimental-transform-types flag to support enums, namespaces, and parameter properties, but this adds complexity. For simpler setups, consider using const objects with as const assertions as an alternative to enums.
For most use cases, no. Node 24's native type-stripping handles direct .ts execution. Tools like ts-node and tsx are optional conveniences that add tsconfig.json support, path alias resolution, and full TypeScript transforms without flags. Use them only if your setup needs those features.
When using native type-stripping, Node executes your .ts files directly, so line numbers in stack traces match your source. For compiled code, enable sourceMap in tsconfig.json and Node will automatically use .js.map files to show original TypeScript locations in errors and debugger sessions.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.