Recommended tsconfig.json Strictness Flags for New Projects
Introduction
Starting a new TypeScript project gives you the chance to set defaults that prevent a large category of bugs before they occur. The set of strictness flags in tsconfig.json—often grouped under the umbrella of "strict mode"—are powerful tools that make your code safer, easier to refactor, and more self-documenting. However, enabling stricter checks can be disruptive if introduced late or without a migration strategy. This tutorial shows intermediate developers which strictness flags to enable in new projects, why each one matters, and how to migrate existing codebases incrementally.
In this guide you'll learn: which flags make the biggest safety improvements, concrete examples of compiler errors and how to fix them, patterns for incremental migration, and useful build and IDE settings to make the strict experience pleasant. We'll cover both the single "strict" switch and the individual flags it controls so you can tailor behavior where needed. The article includes step-by-step examples, small code snippets that illustrate typical errors, and troubleshooting tips for when the compiler's messages are confusing.
By the end of the article you'll be able to create a recommended tsconfig.json for new projects, understand the trade-offs behind each flag, and apply sensible migration strategies to bring older projects to modern TypeScript standards. We'll also link to deeper resources on related topics like module resolution, declaration files, and working with JavaScript libraries so you have a complete playbook for robust TypeScript setups.
Background & Context
TypeScript's type checks are implemented as a set of compiler options exposed in tsconfig.json. Historically, TypeScript grew more permissive by default to reduce friction for JavaScript developers. Over time, the community pushed for safer defaults and the TypeScript team introduced a set of "strictness" options that capture those best practices. The top-level "strict" option toggles a bundle of checks, but you can enable or disable sub-flags individually for fine-grained control.
Adopting stricter flags improves code quality in several ways: it reduces runtime errors by catching mismatches at build time, it documents assumptions via types, and it enables safer refactors because the compiler enforces invariants. When starting a new project, it's best to enable a strong baseline of checks—ideally the full strict set—and then selectively relax specific flags only when you have a clear, documented reason.
If you're new to tsconfig settings or want a refresher on configuring TypeScript projects, see our primer on Introduction to tsconfig.json: Configuring Your Project for foundational guidance.
Key Takeaways
- Enable the
strictumbrella for new projects; it provides the strongest, most future-proof defaults. - Understand the important sub-flags:
noImplicitAny,strictNullChecks,strictFunctionTypes,noUnusedLocals, and more. - Prefer incremental migration for existing codebases: use
--noEmitOnError,skipLibCheck, andincrementalto keep builds productive. - Use
baseUrl/pathsto simplify imports; it reduces mistakes with module resolution. - Keep declaration files and JS interop tidy—use
.d.tsand tools like DefinitelyTyped for third-party libs.
Prerequisites & Setup
Before you follow the examples in this guide, ensure you have the following:
- Node.js (LTS recommended) and npm/yarn
- TypeScript installed locally in your project (npm i -D typescript) or globally (not recommended)
- An editor with TypeScript support (VS Code strongly recommended)
- A basic project scaffold: package.json and an initial tsconfig.json (we'll show example configs below)
If you need more on calling JavaScript from TypeScript or adding JSDoc checks for JS files, check our practical guide on Calling JavaScript from TypeScript and Vice Versa and the guide to Enabling @ts-check in JSDoc for Type Checking JavaScript Files.
Main Tutorial Sections
1) The simplest baseline: strict vs individual flags
The simplest recommended starting point for new projects is to enable the strict umbrella. In tsconfig.json:
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
}
}Setting "strict": true turns on multiple checks including noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, and alwaysStrict. This gives you immediate protection. If a specific check is too noisy, you can turn it off individually (for example "strictPropertyInitialization": false) while keeping others enabled.
For a full discussion of strict mode and recommended flags, see Understanding strict Mode and Recommended Strictness Flags.
2) noImplicitAny — prevent accidental any
noImplicitAny flags errors when the compiler infers the any type implicitly. This is one of the highest ROI flags because implicit any hides type gaps that lead to runtime surprises.
Example:
function flatten(arr) {
return arr.reduce((a, b) => a.concat(b), []);
}
// With noImplicitAny: Error — parameter 'arr' implicitly has an 'any' typeFixes: add explicit types or use generics:
function flatten<T>(arr: T[][]): T[] {
return arr.reduce((a, b) => a.concat(b), [] as T[]);
}If your codebase has many implicit anys, introduce noImplicitAny alongside a migration plan—start by fixing priority modules or files.
Learn more about avoiding untyped variables in our article Using noImplicitAny to Avoid Untyped Variables.
3) strictNullChecks — make null and undefined explicit
strictNullChecks changes how null and undefined are treated: they are only assignable to any, unknown, or explicit union types that include them (e.g., string | null). This prevents ubiquitous null-related runtime errors.
Example error without the flag:
let s: string = maybeString(); // runtime: TypeError: cannot read property 'length' of null
With strictNullChecks:
function maybeString(): string | null { return Math.random() > 0.5 ? "ok" : null; }
let s: string | null = maybeString();
if (s !== null) {
console.log(s.length); // safe
}Common migration strategies:
- Start by opting in for new code only.
- Use
--skipLibCheckwhile you clean third-party types. - Replace
| nullwith| undefinedconsistently where needed.
4) strictFunctionTypes and function variance
strictFunctionTypes enforces sound function parameter bivariance rules—this prevents incorrect assignment of functions that could lead to unexpected behavior.
Example:
type Handler = (event: MouseEvent) => void;
function attach(handler: Handler) {}
// Dangerous assignment prevented
let anyHandler: (e: Event) => void = (e) => console.log(e.type);
attach(anyHandler); // With strictFunctionTypes: ErrorWhy this matters: callback types often cross API boundaries; ensuring variance rules prevents subtle runtime mismatches. You should rarely disable this flag.
5) noImplicitThis and alwaysStrict
noImplicitThis surfaces cases where this has type any. Combined with alwaysStrict, which emits "use strict" in generated code, they improve runtime behavior and catch mistakes.
Example issue:
const obj = {
count: 0,
inc() { this.count++; }
}
const inc = obj.inc;
inc(); // In non-strict JS: this === global -> bugWith noImplicitThis the compiler will warn when this is not typed. You can explicitly annotate this in method signatures:
inc(this: { count: number }) { this.count++; }6) strictPropertyInitialization and class fields
This flag ensures class instance properties are initialized in the constructor or declared as possibly undefined. It prevents errors where a property is accessed before being set.
Example:
class User {
name: string; // Error: Property 'name' has no initializer
constructor() {
// forgot to set name
}
}Fixes:
- Initialize:
name = ''. - Or mark optional:
name?: string. - Or use definite assignment if you initialize later:
name!: string;(use sparingly).
7) noUnusedLocals and noUnusedParameters — keep code clean
These flags catch dead code and unused parameters: they make your codebase easier to refactor and reduce maintenance burden.
Example:
function compute(x: number) {
const y = x + 1; // Error: 'y' is declared but its value is never read
}When migrating, you might enable noUnusedParameters only after cleaning callbacks and API surfaces. But enabling them early helps keep the project tidy.
8) Interoperability with JavaScript: allowJs, checkJs, and declaration files
If your repository mixes JS and TS, allowJs lets the compiler include JavaScript files and checkJs enables type checking for them (with JSDoc annotations). Use skipLibCheck to avoid external type noise when migrating.
tsconfig snippet:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false, // enable selectively
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}For projects consuming JS libraries with no types, consider writing .d.ts files or using DefinitelyTyped. See our guides on Using JavaScript Libraries in TypeScript Projects and Writing a Simple Declaration File for a JS Module for practical patterns. Also consult Troubleshooting Missing or Incorrect Declaration Files in TypeScript when third-party types cause build issues.
9) Module resolution: baseUrl, paths, and avoiding relative hell
Hard-to-maintain import trees often cause mistakes and long relative paths. baseUrl and paths simplify imports and reduce accidental resolution issues.
Example:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"]
}
}
}This lets you import with import { foo } from "@utils/foo"; instead of import { foo } from "../../../utils/foo";. For hands-on patterns and troubleshooting, review Controlling Module Resolution with baseUrl and paths.
10) Build ergonomics: incremental, composite, and source maps
Strictness often improves safety but can increase build times during migration. Use incremental and composite to speed up repeated builds and use sourceMap to make debugging easier.
Example tsconfig snippet:
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"sourceMap": true
}
}Source maps are particularly helpful when chasing runtime errors in compiled output—see our guide to Generating Source Maps with TypeScript (sourceMap option) for debugging tips.
Advanced Techniques (Expert-level tips)
Once you have the basics in place, adopt more advanced patterns to keep strictness sustainable:
- Use
--incrementalandtsc --buildto orchestrate multi-package workspaces safely; combinecompositeprojects to get project references with precise rebuilds. - Apply automated codemods (e.g., ts-migrate or custom scripts) to fix common patterns like implicit anys or uninitialized fields; automate what you can but verify changes manually.
- Employ lint rules that complement TypeScript checks:
typescript-eslintcan catch style and complexity issues that the compiler doesn't. - For large codebases, consider a "strictness debt" board: categorize files by difficulty for making them strict and allocate small iterative milestones.
- Use type-level wrappers for safe interop points: central adapters that cast or validate types when crossing module boundaries. This keeps most of the codebase pure and type-safe.
When third-party types are buggy, skipLibCheck is a pragmatic temporary measure, but prefer to fix or pin the dependency and file issues upstream when possible. For detailed debugging of compiler errors, our Common TypeScript Compiler Errors Explained and Fixed article is a strong companion.
Best Practices & Common Pitfalls
Dos:
- Start new projects with
"strict": trueand only relax specific flags with justification. - Prefer explicit types at module boundaries (exports, public APIs) and allow the compiler to infer internally when appropriate.
- Use
baseUrl/pathsto simplify imports and reduce fragile relative paths. - Keep
skipLibCheckas a temporary migration aid, not a permanent escape hatch. - Write
.d.tsfiles for untyped JS modules and contribute fixes to DefinitelyTyped when useful.
Don'ts:
- Don’t use
anyas a blanket escape; preferunknownif you must defer typing and handle it explicitly. - Avoid sprinkling
// @ts-ignoreas a long-term strategy; track each ignore and remove them as types improve. - Don’t disable all strict options to silence build warnings; this hides real issues and makes later fixes much harder.
Troubleshooting tips:
- If the compiler is noisy after enabling strict flags, enable them in smaller batches and run the codebase tests after each batch.
- Use the TypeScript language server logs in your editor to diagnose slow intellisense caused by heavy type computations.
- For confusing errors, reduce the example to a minimal repro and use the TypeScript Playground to iterate quickly.
If you want to dive deeper into declaration files and typing existing JS libraries, see our guides: Introduction to Declaration Files (.d.ts): Typing Existing JS and Declaration Files for Global Variables and Functions.
Real-World Applications
Applying strict flags benefits many real-world scenarios:
- Backend services (Node.js): strict null checks and
noImplicitAnyprevent runtime type errors that can crash services. UsesourceMapandincrementalfor faster development loops. - Frontend apps (React/Vue):
strictFunctionTypesandnoImplicitThisavoid subtle bugs in event handling and callbacks.baseUrl/pathskeeps large component trees manageable. - Libraries and SDKs: enabling
declarationandstrictensures consumers get accurate type information. Package authors should test withskipLibCheckoff to ensure robust public types.
When integrating third-party JS or moving from JS to TS, leverage the techniques in Migrating a JavaScript Project to TypeScript (Step-by-Step) to structure the transition and minimize breakage.
Conclusion & Next Steps
For new projects, make "strict": true your default and selectively relax individual flags only when you have a clear reason. Use incremental migration, tooling, and proper tsconfig settings to keep builds fast and developer ergonomics high. Next steps: apply the recommended tsconfig to a small sample project, run the compiler, and fix the first set of errors to get comfortable with the workflow.
Recommended reading: deep dives on module resolution, declaration files, and common compiler errors linked throughout this article.
Enhanced FAQ
Q1: Should I always set "strict": true for new projects?
A1: Yes. For new projects you should enable "strict": true as a sensible default because it provides the best trade-off between safety and developer effort. If a specific strict sub-flag causes unreasonable friction, you can disable it individually with an inline comment in tsconfig and document why.
Q2: How do I migrate a large codebase to strict mode without breaking everything? A2: Use an incremental plan:
- Add
"strict": trueto a branch. - Enable
skipLibCheckto avoid third-party noise. - Run the compiler and categorize errors (e.g., implicit anys, null checks, property initialization).
- Triage files into "easy" and "hard" buckets. Fix easy files first.
- Use
// @ts-expect-erroror temporaryanywith TODOs for hard cases, then iterate. - Consider
tsc --buildwith project references for monorepos. Our migration guide Migrating a JavaScript Project to TypeScript (Step-by-Step) contains practical steps for staged adoption.
Q3: What's the difference between noImplicitAny and explicitly using any?
A3: noImplicitAny disallows implicit any inferred by the compiler, encouraging explicit types. Using any explicitly is allowed even with noImplicitAny off; however, explicit any bypasses type checking and should be used sparingly. Prefer unknown when receiving untrusted values so you force explicit narrowing.
Q4: When should I use skipLibCheck?
A4: skipLibCheck is a pragmatic stopgap used during migration to avoid failures from broken third-party type declarations. It's useful while you clean or replace problematic dependencies. For published libraries or final builds, prefer turning it off and fixing the types where possible. For debugging third-party type issues, consult Using DefinitelyTyped for External Library Declarations.
Q5: How do baseUrl and paths interact with bundlers like Webpack?
A5: baseUrl and paths only affect TypeScript's module resolution; bundlers must be configured to mirror these mappings (e.g., using Webpack's resolve.alias or TypeScript path plugins). For examples and troubleshooting, see Controlling Module Resolution with baseUrl and paths.
Q6: I enabled strictPropertyInitialization and the compiler complains a lot. What should I do?
A6: You have options:
- Initialize properties with default values.
- Mark properties optional using
?if they can legitimately be absent. - Use definite assignment assertion (
!) when you can guarantee initialization occurs before use (use sparingly). - Move initialization logic into constructors so the compiler can verify assignments.
Q7: Are there performance trade-offs to enabling all strictness flags?
A7: Type checking can be slightly heavier with stricter options, particularly in large codebases using complex type computations. Mitigate this with incremental, composite projects, project references, and editor settings to limit in-editor type checking scope. For troubleshooting slow compiler or editor performance, our article on common compiler errors and debugging steps is helpful: Common TypeScript Compiler Errors Explained and Fixed.
Q8: How should I handle third-party JS libraries with no types? A8: Options include:
- Install
@types/packages from DefinitelyTyped if available. See Using DefinitelyTyped for External Library Declarations. - Write a minimal
.d.tsdeclaration for the parts you use. See Writing a Simple Declaration File for a JS Module. - Use runtime validation at the boundary (e.g., zod, io-ts) and type assertions internally.
- Enable
allowJsandcheckJsselectively for files where you want JSDoc-based checking. For JSDoc patterns, read Enabling @ts-check in JSDoc for Type Checking JavaScript Files.
Q9: What are practical examples of using incremental and composite?
A9: incremental stores build info to speed up subsequent compilations. composite is required for project references in monorepos. Use composite: true on packages that other packages depend on and then use tsc --build to orchestrate building the graph efficiently. Combine these options with outDir and tsBuildInfoFile to optimize CI and local dev loops.
Q10: Where can I find more hands-on resources about these settings? A10: Useful companion resources include detailed guides on tsconfig basics Introduction to tsconfig.json: Configuring Your Project, module resolution Controlling Module Resolution with baseUrl and paths, and troubleshooting third-party declaration issues Troubleshooting Missing or Incorrect Declaration Files in TypeScript.
