A thoughtful tsconfig.json turns TypeScript from a type-checking add-on into a dependable part of your development workflow. This guide provides a reusable checklist for choosing strict compiler options, module settings, path rules, and build boundaries for frontend apps, Node.js services, and reusable libraries.
Overview
TypeScript configuration is a set of trade-offs between safety, compatibility, build speed, and runtime expectations. There is no single correct tsconfig.json for every project: a browser application, a Node.js API, and a published package usually need different module and output settings.
Start by separating three concerns:
- Type checking: rules that identify unsafe or incomplete code.
- Module and language compatibility: how TypeScript interprets imports and emits JavaScript.
- Project boundaries: which files are included, excluded, built, and referenced.
A useful baseline is to enable strict checking first, then make deliberate exceptions when a framework, runtime, or migration requirement justifies them. Keep the configuration close to the project that owns it, and use comments or documentation for options that are not obvious to future maintainers.
If you are migrating an existing JavaScript codebase, avoid changing every setting at once. The JavaScript to TypeScript migration checklist can help you introduce type checking in stages while keeping the project buildable.
Checklist by scenario
For frontend applications
Frontend projects commonly compile source code for a bundler rather than producing a complete deployable directory with tsc. A practical starting point is:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src"]
}Use the module resolution mode expected by your bundler and framework. The jsx value should match the JSX transform used by the application. noEmit is appropriate when another tool performs the actual JavaScript and asset build; it does not mean TypeScript is unnecessary, because the compiler still checks the project.
Consider noUncheckedIndexedAccess for applications that read from arrays or maps based on user input. It makes potentially missing values visible in types, so code such as items[index] must be handled as possibly undefined.
For Node.js applications
Node.js projects need configuration that agrees with the package's module format and the way the application is started. For a project that emits JavaScript directly, a baseline might look like this:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"sourceMap": true
},
"include": ["src"]
}Treat module, moduleResolution, and the package metadata as one decision. Import syntax, file extensions, and the package's module declaration can affect how emitted code runs. Test the compiled output with the same command and runtime assumptions used in deployment.
rootDir and outDir make the source-to-build relationship explicit. Keep tests, scripts, generated files, and configuration files out of the production compilation unless they genuinely belong there.
For libraries and packages
Libraries should be checked against the public API they expose, not only against their internal source. Use a build configuration that emits declarations when consumers need them:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}Many libraries use a separate bundler or build tool for JavaScript output. In that setup, TypeScript may be responsible primarily for declarations and type checking. Avoid publishing internal path aliases unless your package's build process rewrites them into valid consumer-facing imports.
For multi-package repositories
When separate packages have independent build outputs, consider project references. A root configuration can coordinate smaller project configurations:
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/web" }
]
}Referenced projects generally need composite enabled and a clear output or build-info location. References can make dependency boundaries explicit and support incremental builds, but they add configuration that should be justified by the repository's size and workflow. See the TypeScript project references guide before introducing them.
What to double-check
strict: Enable the strict family as a baseline. If a migration requires a temporary exception, record why and define a path to remove it.strictNullChecks: This option makesnullandundefinedmeaningful parts of types. Check boundaries such as API responses, environment variables, and DOM lookups carefully.noImplicitAny: Prevent untyped parameters and inferredanyvalues from spreading through the codebase.noUncheckedIndexedAccess: Use it when missing array or dictionary entries are realistic. Expect to add explicit existence checks.exactOptionalPropertyTypes: Consider it when the difference between an omitted property and a property explicitly set toundefinedmatters to an API.target: Choose the JavaScript language level supported by your actual runtime or browser support policy. Do not choose it based only on the syntax used in source files.moduleandmoduleResolution: Match the runtime, bundler, and package metadata. Incorrect combinations often produce import errors that look unrelated to TypeScript.pathsandbaseUrl: Treat aliases as an editor and compiler convenience, not automatic runtime behavior. Configure the bundler, test runner, and production runtime consistently.lib: Confirm that the included type definitions match the environments your code uses, such as browser APIs, server APIs, or both.types: Restrict automatically included global type packages when test and production globals are becoming mixed.skipLibCheck: It can reduce noise and speed up checking, but it does not repair incompatible application types. Reconsider it when diagnosing a dependency type conflict.
When a compiler error appears after tightening configuration, fix the underlying boundary rather than silencing the whole rule. For practical narrowing patterns, refer to TypeScript error fixes for type narrowing and common compiler messages.
Common mistakes
Using one configuration for every job
A single file can become overloaded with test globals, browser libraries, server output settings, and package publishing options. Prefer a shared base configuration plus focused project configurations when the concerns are genuinely different.
Assuming aliases work at runtime
TypeScript can understand an alias while Node.js or a test runner cannot. Every alias needs an equivalent runtime or bundler arrangement, or it should be replaced with a relative or package import.
Confusing noEmit with no build
noEmit disables TypeScript's JavaScript output; it does not replace a bundler, transpiler, or deployment build. Make the tool responsible for each stage explicit in package scripts.
Excluding files too broadly
An exclude entry does not necessarily override every imported file. If source code imports a file from outside the intended boundary, TypeScript may still follow it. Keep include narrow and inspect the project with tsc --showConfig when behavior is surprising.
Changing strictness without checking generated declarations
For libraries, stricter checking can alter inferred public types. Review declaration output and add explicit API annotations where inference exposes implementation details. The TypeScript mapped types guide is useful when public types need deliberate transformations.
When to revisit
Review tsconfig.json before a seasonal planning cycle, a major dependency upgrade, a runtime change, or a build-tool migration. Also revisit it when workflows change: adding a test runner, introducing a workspace, splitting a package, moving from a bundler to direct Node.js execution, or adopting a new deployment target.
Use this short review routine:
- Run the project's type-check command with no emitted output and confirm which configuration it uses.
- Inspect the effective settings with
tsc --showConfig. - Verify that module settings agree with package metadata, bundler settings, and runtime behavior.
- Check whether new source, test, generated, or declaration files are included unintentionally.
- Review compiler errors caused by stricter options instead of adding broad suppressions.
- For libraries, inspect emitted declarations and test the package from a separate consumer project.
- Document temporary exceptions and assign them a removal condition.
Keep this checklist alongside your project setup notes. A good TypeScript configuration is not the one with the most options; it is the one that makes the project's runtime assumptions, safety boundaries, and build responsibilities clear.