tsconfig project references
Three tsconfig files per project — root / app / node — extending a single shared base. Why each one exists and what goes in it.
Every project in the monorepo ships three tsconfig.*.json files:
| File | Purpose |
|---|---|
tsconfig.json | Solution-style root. Has no compilerOptions; just references: to the two below. |
tsconfig.app.json | The runtime code — app/, src/, MDX, mdx-components.tsx. DOM lib. JSX preserved. |
tsconfig.node.json | Build / config files — *.config.{ts,js}, postcss.config.js. Node lib. No DOM. |
This split is mandatory for tsgo --build to work cleanly. Without it, the
Node-side config files (which import node:fs, node:path) get pulled into
the DOM-lib compilation and trigger spurious errors.
The shared base
@todayai-labs/tsconfig
holds everything that should be consistent across the repo:
{
"compilerOptions": {
"composite": true,
"incremental": true,
"noEmitOnError": true,
"target": "ESNext",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true
}
}The two notable choices:
composite: true— required for project references; emits.d.tsplus a.tsbuildinfofor incremental rebuildsnoUncheckedIndexedAccess: true— everyarr[i]andobj[key]returnsT | undefined. Aggressive but correct; protects against most off-by-one and missing-key bugs. Usearr.at(i)or destructure with defaults.
Solution root (tsconfig.json)
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}This file is not a typecheck input — it has no compilerOptions, no
include:. Its only job is telling tsgo --build "this project has two
sub-projects". The root pnpm exec tsgo --build walks every project's
tsconfig.json and recurses.
App tsconfig (tsconfig.app.json)
Where the actual app or library code is checked:
{
"extends": "@todayai-labs/tsconfig/base.json",
"compilerOptions": {
"noEmit": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"rootDir": ".",
"outDir": "dist/ts/app",
"tsBuildInfoFile": "dist/ts/app/.tsbuildinfo",
"paths": {
"@/*": ["./*"]
}
},
"include": ["app", "lib", "src", ".source/**/*.ts"]
}noEmit: true is what makes this a typecheck-only project — the actual
runtime build is Next.js (Turbopack) or Vite, not tsgo. composite
inherited from the base still emits .tsbuildinfo for incremental work.
paths is per-project so each app can have its own @/* alias.
Node tsconfig (tsconfig.node.json)
For build-side files:
{
"extends": "@todayai-labs/tsconfig/base.json",
"compilerOptions": {
"noEmit": true,
"lib": ["ESNext"],
"types": ["node"],
"outDir": "dist/ts/node",
"tsBuildInfoFile": "dist/ts/node/.tsbuildinfo"
},
"include": ["*.config.ts", "*.config.js", "postcss.config.js"],
"exclude": ["source.config.ts"]
}types: ["node"] is explicit — the base config sets types: [] so the
DOM-side tsconfig.app.json doesn't accidentally pick up @types/node and
let server-only globals leak into client code.
For Next.js docs sites, source.config.ts (the Fumadocs MDX config) is
excluded from the Node tsconfig and included in the app tsconfig instead
— it's read at build time but the types it references live in the Fumadocs
client surface.
Root tsconfig.json registry
The repo-root tsconfig.json
lists every project for tsgo --build:
{
"files": [],
"references": [
{ "path": "./apps/web/tsconfig.json" },
{ "path": "./apps/admin/tsconfig.json" },
{ "path": "./apps/dev-docs/tsconfig.json" },
{ "path": "./design-system/ui/tsconfig.json" }
]
}When you add a new project, you must add it here. Forgetting this is the
single most common reason a new package's typecheck is mysteriously skipped
on CI but passes locally with pnpm --filter scoped runs.
scripts/affected.ts does not discover this file's references — it
walks pnpm-workspace.yaml instead. So Moon and pnpm find your new project
automatically; only the typecheck graph needs the manual entry.