A well-designed tsconfig.json makes TypeScript predictable across editors, CI, build tools, and deployment. This practical checklist explains the compiler options that matter most, provides starter configurations for common project types, and shows what to review when your toolchain changes.
Overview
TypeScript configuration is more than a collection of switches. It defines how source files are discovered, which JavaScript features the compiler assumes, how modules are resolved, whether declarations are produced, and how much uncertainty the type checker permits. A configuration that works for a browser application may be unsuitable for a published library or a Node.js service.
The safest approach is to treat tsconfig.json as part of your project architecture. Keep the configuration small enough to understand, make strictness explicit, and confirm that the compiler, bundler, test runner, and runtime agree about module resolution and output.
Before changing an option, answer three questions:
- Is this setting for type checking, JavaScript emission, or both?
- Does another tool also need to understand the same assumption?
- Will this affect consumers of the project, or only local development?
For larger applications, pair this work with a deliberate TypeScript project structure. For runtime data from APIs or environment variables, remember that TypeScript types disappear at runtime; use a validation boundary such as the patterns described in this TypeScript runtime validation guide.
Checklist by scenario
1. Shared baseline for application code
Start with strict checking unless there is a documented migration reason not to. strict enables a group of checks, including safer handling of nullable values and function parameters. It usually exposes real edge cases rather than creating runtime guarantees, so resolve errors incrementally instead of silencing them with broad escape hatches.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}
This is a reasonable starting point for a bundler-managed application. noEmit tells TypeScript to type-check without writing JavaScript, leaving emission to a tool such as a framework or bundler. skipLibCheck can reduce noise from declaration files in dependencies, but it does not replace checking your own source.
2. Node.js service
A Node.js service needs module settings that match how it runs in production. Do not select CommonJS or native ECMAScript modules based only on a tutorial; check the package metadata, runtime command, loader, and build output together. If TypeScript emits JavaScript, use an output directory and make the source root clear.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"declaration": false
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
sourceMap connects generated JavaScript back to TypeScript during debugging. forceConsistentCasingInFileNames helps catch imports whose capitalization behaves differently across file systems. If your service uses a separate bundler or transpiler, compare this configuration with the settings in that tool rather than assuming tsc controls the complete build.
3. React or Next.js application
Frameworks commonly provide a base configuration and may update it as part of their build process. Extend the framework configuration when appropriate instead of replacing it without understanding what it contains. Keep application-specific choices—such as aliases, strictness, and included directories—visible in the project configuration.
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "next-env.d.ts"]
}
A path alias is only a TypeScript and editor feature until the framework, bundler, test runner, and runtime resolve the same alias. Review the detailed TypeScript path alias guide before adding aliases to a project with multiple tools. For framework-specific organization and data typing, see this Next.js project configuration guide.
4. Published TypeScript library
A library has two outputs to consider: JavaScript for consumers and declaration files that describe its public API. Use a dedicated build configuration so application-only settings do not leak into the package.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"isolatedModules": true
},
"include": ["src"]
}
Review the generated declaration files as part of a library release. Public types should not accidentally expose private implementation paths or types that consumers cannot install. The bundler and package metadata must also agree on supported module formats and entry points.
5. Monorepo with project references
In a monorepo, separate shared settings from package-specific settings. A base configuration can hold strictness and naming rules, while each package controls its own files and outputs. Project references can express build dependencies between packages and support build-mode workflows.
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
Referenced projects generally need composite enabled and a predictable output or declaration strategy. Keep the root configuration as an orchestration file rather than adding all source files to it. This makes package boundaries clearer and avoids compiling the same files through several project definitions.
What to double-check
target: This controls the JavaScript language level TypeScript emits. Choose it according to the environments that execute the output, not the syntax your editor supports.moduleandmoduleResolution: These describe how imports are emitted and found. Keep them aligned with your runtime, package metadata, framework, and bundler.include,exclude, andfiles: Confirm that tests, scripts, generated files, and configuration files are intentionally included or excluded. An excluded directory may still be pulled into the program through an imported file.rootDirandoutDir: Check that emitted paths preserve the directory layout you expect and that generated output is not included in the next compilation.baseUrlandpaths: Treat aliases as a cross-tool contract. Test them in the editor, type checker, build, tests, and production runtime.- Declarations and maps: Libraries generally need
declaration; debugging workflows often benefit fromsourceMap. Decide whether declaration maps belong in the published package. - Incremental compilation:
incrementalcan store build information to reduce repeated work. Keep the cache location out of source control and verify that CI caching does not reuse invalid results. - Runtime validation: Compiler types do not validate JSON, request bodies, or environment variables. Add schemas at external boundaries; the environment variable validation guide covers a practical example.
Common mistakes
Using noEmit in a library build. This is useful when another tool emits application code, but it prevents tsc from producing the JavaScript or declarations a library may require.
Assuming aliases change runtime imports. TypeScript can accept an alias while Node.js or a test runner fails to resolve it. Configure every consumer or prefer an approach your build stack supports consistently.
Disabling strictness globally to finish a migration. A temporary relaxed configuration can be a useful migration step, but record the remaining work and avoid making new files less safe. Options such as allowJs and checkJs can help stage a JavaScript-to-TypeScript migration without pretending that untyped boundaries are typed.
Copying framework defaults without ownership. Defaults change with project tooling. Keep a short explanation for non-obvious overrides and inspect extended configurations when diagnosing an error.
Confusing compile-time and runtime safety. A successful type check does not prove that an API returned the expected shape. Use discriminated unions for modeled states, and runtime schemas for untrusted data.
When to revisit
Review tsconfig.json whenever the project changes how it builds or runs. That includes a TypeScript upgrade, a new framework or bundler, a Node.js runtime change, a move between CommonJS and ECMAScript modules, the introduction of a monorepo, or a change in package publishing strategy.
Before a seasonal planning or maintenance cycle, use this short review:
- Run the type checker and build from a clean checkout.
- Confirm that editor, CI, tests, bundler, and runtime use the intended configuration.
- Inspect aliases, generated files, declaration output, and source-map behavior.
- Check that strictness has not been weakened by a new override.
- Remove obsolete options, duplicate settings, and caches from version control.
- Document any deliberate exception and add a follow-up issue if it is temporary.
When tools change, revisit the configuration rather than adding another workaround. A small, explicit tsconfig hierarchy is easier to migrate, easier to review, and more likely to keep type-safe JavaScript development consistent across the whole project.