This is possible, and it turns out, not hard.
The reason the solution is not obvious is because typescript relies on the rootDir
to decide the directory structure of the output (see this comment from Typescript’s bossman), and only code included in the output or in package dependencies can be imported.
- If you set
rootDir
to the root of your project,package.json
gets emitted to the root ofoutDir
and can be imported. But then your compiledsrc
files get written tooutDir/src
. - If you set
rootDir
tosrc
, files in there will compile to the root ofoutDir
. But now the compiler won’t have a place to emitpackage.json
, so it issues “an error because the project appears to be misconfigured” (bossman’s words).
solution: use separate Typescript sub-projects
A Typescript project is defined by a tsconfig file, is self-contained, and is effectively bounded by its rootDir
. This is a very good thing, as it lines up with principles of encapsulation.
You can have multiple projects (e.g. a main and a set of libs) each in their own directory and with their own tsconfig. Dependencies between them are declared in the tsconfig file using Typescript Project References.
I admit, the term “projects” is a poor one, as intuitively it refers to the whole shebang, but “modules” and “packages” are already taken in this context. Think of them as “subprojects” and it will make more sense.
We’ll treat the src
directory and the root directory containing package.json
as separate projects. Each will have its own tsconfig
file.
-
Give the
src
dir its own project../src/tsconfig.json
:{ "compilerOptions": { "rootDir": ".", "outDir": "../dist/", "resolveJsonModule": true }, "references": [ // this is how we declare a dependency from { "path": "../" } // this project to the one at the root dir` ] }
-
Give the root dir its own project.
./tsconfig.json
:{ "compilerOptions": { "rootDir": ".", "outDir": ".", // if out path for a file is same as its src path, nothing will be emitted "resolveJsonModule": true, "composite": true // required on the dependency project for references to work }, "files": [ // by whitelisting the files to include, TS won't automatically "package.json" // include all source below root, which is the default. ] }
-
run
tsc --build src
and voilà!This will build the
src
project. Because it declares a reference to the root project, it will build that one also, but only if it is out of date. Because the root tsconfig has the same dir as theoutDir
, tsc will simply do nothing topackage.json
, the one file it is configured to compile.
this is great for monorepos
-
You can isolate modules/libraries/sub-projects by putting them in their own subdirectory and giving them their own tsconfig.
-
You can manage dependencies explicitly using Project References, as well as modularize the build:
From the linked doc:
-
you can greatly improve build times
A long-awaited feature is smart incremental builds for TypeScript projects. In 3.0 you can use the
--build
flag withtsc
. This is effectively a new entry point fortsc
that behaves more like a build orchestrator than a simple compiler.Running
tsc --build
(tsc -b
for short) will do the following:- Find all referenced projects
- Detect if they are up-to-date
- Build out-of-date projects in the correct order
Don’t worry about ordering the files you pass on the commandline –
tsc
will re-order them if needed so that dependencies are always built first. -
enforce logical separation between components
-
organize your code in new and better ways.
-
It’s also very easy:
-
A root
tsconfig
for shared options and to build all
subprojects with a simpletsc --build
command
(with--force
to build them from scratch)src/tsconfig.json
{ "compilerOptions": { "outDir": ".", // prevents this tsconfig from compiling any files // we want subprojects to inherit these options: "target": "ES2019", "module": "es2020", "strict": true, ... }, // configure this project to build all of the following: "references": [ { "path": "./common" } { "path": "./projectA" } ] }
-
A “common” library that is prevented from importing from the
other subprojects because it has no project referencessrc/common/tsconfig.json
{ "extends": "../tsconfig.json", //inherit from root tsconfig // but override these: "compilerOptions": { "rootDir": ".", "outDir": "../../build/common", "resolveJsonModule": true, "composite": true } }
-
A subproject that can import common because of the declared reference.
src/projectA/tsconfig.json
{ "extends": "../tsconfig.json", //inherit from root tsconfig // but override these: "compilerOptions": { "rootDir": ".", "outDir": "../../build/libA", "resolveJsonModule": true, "composite": true }, "references": [ { "path": "../common" } ] }