Back

TypeScript in Node: The Practical Setup

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 tsc compilation 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 type for type-only imports to avoid runtime errors with type-stripping
  • Run tsc --noEmit in 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: NodeNext and moduleResolution: NodeNext: Matches Node’s actual module resolution behavior
  • verbatimModuleSyntax: Requires explicit import type for 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 is utils.ts
  • Respects ESM vs CJS rules: Your package.json type 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:

  • .mts files → always ESM
  • .cts files → always CommonJS
  • .ts files → follow package.json type field

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.

OpenReplay