CodeFixesHub
    programming tutorial

    Recommended tsconfig.json Strictness Flags for New Projects

    Harden TypeScript with recommended tsconfig strictness flags. Practical examples, migration tips, and commands—apply safer defaults today.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 26
    Published
    22
    Min Read
    2K
    Words
    article summary

    Harden TypeScript with recommended tsconfig strictness flags. Practical examples, migration tips, and commands—apply safer defaults today.

    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 strict umbrella 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, and incremental to keep builds productive.
    • Use baseUrl/paths to simplify imports; it reduces mistakes with module resolution.
    • Keep declaration files and JS interop tidy—use .d.ts and 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:

    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:

    ts
    function flatten(arr) {
      return arr.reduce((a, b) => a.concat(b), []);
    }
    
    // With noImplicitAny: Error — parameter 'arr' implicitly has an 'any' type

    Fixes: add explicit types or use generics:

    ts
    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:

    ts
    let s: string = maybeString();
    // runtime: TypeError: cannot read property 'length' of null

    With strictNullChecks:

    ts
    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 --skipLibCheck while you clean third-party types.
    • Replace | null with | undefined consistently 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:

    ts
    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: Error

    Why 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:

    ts
    const obj = {
      count: 0,
      inc() { this.count++; }
    }
    
    const inc = obj.inc;
    inc(); // In non-strict JS: this === global -> bug

    With noImplicitThis the compiler will warn when this is not typed. You can explicitly annotate this in method signatures:

    ts
    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:

    ts
    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:

    ts
    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:

    json
    {
      "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:

    json
    {
      "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:

    json
    {
      "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 --incremental and tsc --build to orchestrate multi-package workspaces safely; combine composite projects 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-eslint can 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": true and 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/paths to simplify imports and reduce fragile relative paths.
    • Keep skipLibCheck as a temporary migration aid, not a permanent escape hatch.
    • Write .d.ts files for untyped JS modules and contribute fixes to DefinitelyTyped when useful.

    Don'ts:

    • Don’t use any as a blanket escape; prefer unknown if you must defer typing and handle it explicitly.
    • Avoid sprinkling // @ts-ignore as 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 noImplicitAny prevent runtime type errors that can crash services. Use sourceMap and incremental for faster development loops.
    • Frontend apps (React/Vue): strictFunctionTypes and noImplicitThis avoid subtle bugs in event handling and callbacks. baseUrl/paths keeps large component trees manageable.
    • Libraries and SDKs: enabling declaration and strict ensures consumers get accurate type information. Package authors should test with skipLibCheck off 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": true to a branch.
    • Enable skipLibCheck to 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-error or temporary any with TODOs for hard cases, then iterate.
    • Consider tsc --build with 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:

    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.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:19:52 PM
    Next sync: 60s
    Loading CodeFixesHub...