Quick start · project shape

Type-checked JavaScript for Node.js with CommonJS

This is the setup for a Node.js project written as CommonJS: "type": "commonjs" in package.json (or no "type" at all), require and module.exports in your .js files. TypeScript understands that style in JavaScript files: a require call is an import, and module.exports and exports.name are exports, typed from your JSDoc.

  • Node.js (CommonJS)
  • 2 dev dependencies
  • npm run lint

The config is the same as for ES modules. module: "nodenext" reads "type" from package.json and checks each file the way Node.js will load it. The files below are one project, checked with TypeScript 5.9 when this site is built.

Install

TypeScript is a development dependency: it checks your code and never runs in production. @types/node gives the checker the types of Node's built-in modules (node:fs, node:http, process and the rest). Pick the @types/node major version that matches the Node.js you run, for example @types/node@24 for Node.js 24.

Terminal
npm install --save-dev typescript @types/node

jsconfig.json

Identical to the ES modules setup: nodenext takes the module format from package.json, so the same config checks either kind of project, and a project that mixes .cjs and .mjs files.

jsconfig.json
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src/**/*.js"]
}

npm scripts

"type": "commonjs" is also what Node.js assumes when there is no "type"; writing it makes the choice explicit, as Node.js's documentation recommends. lint checks once; lint:watch checks again on every save.

package.json
{
  "type": "commonjs",
  "scripts": {
    "lint": "tsc -p jsconfig.json",
    "lint:watch": "tsc -p jsconfig.json --watch --preserveWatchOutput"
  }
}

What the checker reads from require and module.exports

In a CommonJS .js file:

  • const { formatPrice } = require("./format") is an import; a name the module does not export is an error, with a suggestion (TS2724), as below
  • relative require calls may leave out .js, because Node.js tries the extension for CommonJS
  • module.exports = { … } and exports.name = … are the module's exports, typed from the JSDoc on what you assign
  • since TypeScript 5.8, require() of an ES module is allowed under nodenext, as in Node.js from 20.19 and 22.12; Node.js still refuses an ES module that uses top-level await
src/main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: src/main.js(3,9): error TS2724: '"./format"' has no exported member named 'formatPrise'. Did you mean 'formatPrice'? 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. src/main.js(3,9): error TS2724: '"./format"' has no exported member named 'formatPrise'. Did you mean 'formatPrice'? 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs from 5.9.3. src/main.js(3,9): error TS2305: Module '"./format"' has no exported member 'formatPrise'. checked 21 October 2026
const { createServer } = require("node:http");
const { formatPrise } = require("./format");error TS2724: '"./format"' has no exported member named 'formatPrise'. Did you mean 'formatPrice'?

const port = Number(process.env.PORT ?? 3000);

createServer((req, res) => {
  res.end(`Today's special: ${formatPrise(1999)}`);
}).listen(port);
A misspelled name in a destructured require.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.js"
  ]
}

The example project

src/format.js exports a typed helper with module.exports; src/main.js requires it and Node's http module and starts a server.

src/main.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
const { createServer } = require("node:http");
const { formatPrice } = require("./format");

const port = Number(process.env.PORT ?? 3000);

createServer((req, res) => {
  res.end(`Today's special: ${formatPrice(1999)}`);
}).listen(port);
The server, which loads a helper with require.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.js"
  ]
}
src/format.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @param {number} cents
 * @returns {string}
 */
function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}

module.exports = { formatPrice };
The helper, typed with JSDoc and exported with module.exports.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.js"
  ]
}

Under TypeScript 6.0 and 7.0

Every example on this page also runs under TypeScript 6.0.3 and 7.0.2 when this site is built. One of them reports something different from 5.9.3, the version the page describes; the ✗ on its badge links to its entry below.

A misspelled require name loses its hint

TypeScript 7.0 · other checker changes

The pattern: const { formatPrise } = require("./format") for an export named formatPrice.

Why: 5.9 reports TS2724 with "Did you mean 'formatPrice'?"; TypeScript 7.0.2 reports TS2305, the same missing export without the suggestion. The port's list of changes does not mention it. Source: CHANGES.md: changes between TypeScript 6 and the native port.

A misspelled name in a destructured require src/main.js

TypeScript 5.9.3
src/main.js(3,9): error TS2724: '"./format"' has no exported member named 'formatPrise'. Did you mean 'formatPrice'?
TypeScript 6.0.3same as 5.9.3

The same errors as 5.9.3.

TypeScript 7.0.2differs from 5.9.3
src/main.js(3,9): error TS2305: Module '"./format"' has no exported member 'formatPrise'.

What works in every version: Fix the name; nothing else changes.

The example The server, which loads a helper with require on this page shows it, with the same result under every version.

In your editor

VS Code reads this jsconfig.json with no further setup. To make it use the same TypeScript as npm run lint, see VS Code.

Sources

Last reviewed 20 October 2026.