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.
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.
{
"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.
{
"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.
{
"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:
import { formatPrice } from "@acme/util";
console.log(formatPrice("19.99"));error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.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.
import { formatPrice } from "@acme/util";
console.log(formatPrice(1999));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"
]
} /**
* @param {number} cents
* @returns {string}
*/
export function formatPrice(cents) {
return `$${(cents / 100).toFixed(2)}`;
}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
- TSConfig Reference: extends · TypeScript
- TSConfig Reference: paths · TypeScript
- Project References · TypeScript handbook
- workspaces · npm docs
Last reviewed 20 October 2026.