Quick start · project shape

Type-checked JavaScript in a monorepo

In a repository with several packages, the simplest checked setup is one program for everything: a root jsconfig.json includes every package's source, so npm run lint at the root checks all of them and every import between them. Each package also gets a small jsconfig.json that extends the root, so an editor checks a file with its own package's settings.

  • Monorepo (workspaces)
  • 2 dev dependencies
  • npm run lint

Imports between packages use package names (@acme/util). paths maps those names to the packages' sources, so the checker resolves them with or without npm install. The files below are one project, checked with TypeScript 5.9 when this site is built.

Install

Development dependencies live in the root package.json, once for all packages. npm workspaces ("workspaces": ["packages/*"]) link each package into node_modules under its name when you run npm install.

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

jsconfig.json

include takes every package's src folder. paths maps @acme/<name> to packages/<name>/src/index.js, relative to this file. With the workspaces installed, TypeScript would also find @acme/util through its link in node_modules, and in our test it then still checked the linked package's files; paths makes the result independent of whether npm install has run.

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

npm scripts

One lint at the root checks the whole repository in one program, which is faster than checking each package separately and sees every import between them.

package.json
{
  "private": true,
  "type": "module",
  "workspaces": ["packages/*"],
  "scripts": {
    "lint": "tsc -p jsconfig.json",
    "lint:watch": "tsc -p jsconfig.json --watch --preserveWatchOutput"
  }
}

One small config per package

Each package's jsconfig.json extends the root and narrows include to its own source. Relative paths set in the root config, such as those in paths, stay relative to the root file. This site also checks both package configs on every build.

packages/app/jsconfig.json
{
  "extends": "../../jsconfig.json",
  "include": ["src/**/*.js"]
}

Checking across packages

A call into another package is checked against that package's JSDoc, exactly like an import within one package:

packages/app/src/main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: packages/app/src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. packages/app/src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. packages/app/src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. checked 21 October 2026
import { formatPrice } from "@acme/util";

console.log(formatPrice("19.99"));error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
A wrong argument across the package boundary.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true,
    "paths": {
      "@acme/*": [
        "./packages/*/src/index.js"
      ]
    }
  },
  "include": [
    "packages/*/src/**/*.js"
  ]
}

Project references and composite

TypeScript's project references split a repository into projects that tsc -b checks in order. They are built for projects that write output: a referenced project needs composite, which requires declaration files, and tsc -b refuses to reference a project with noEmit (TS6310, "Referenced project may not disable emit", in our test). For JavaScript that would mean writing .d.ts files for every package on every check. Use them only if the packages publish types anyway, as in the library setup; otherwise one root program is simpler.

paths and each package's exports

paths sends the checker straight to src/index.js, past the package's exports map. Keep exports pointing at the same file, so what the checker reads is what Node.js loads through the workspace link.

The example project

Two packages: @acme/util exports a typed helper, @acme/app imports it by name.

packages/app/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
import { formatPrice } from "@acme/util";

console.log(formatPrice(1999));
The app package imports the util package by its name.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true,
    "paths": {
      "@acme/*": [
        "./packages/*/src/index.js"
      ]
    }
  },
  "include": [
    "packages/*/src/**/*.js"
  ]
}
packages/util/src/index.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}
 */
export function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}
The util package that the app imports.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": [
      "node"
    ],
    "skipLibCheck": true,
    "paths": {
      "@acme/*": [
        "./packages/*/src/index.js"
      ]
    }
  },
  "include": [
    "packages/*/src/**/*.js"
  ]
}

In your editor

VS Code checks each file with the nearest jsconfig.json above it, which is its package's. Open the repository root as the workspace so every package's config is found.

Sources

Last reviewed 20 October 2026.