TypeScript strict vs loose: when to use which

Status: đźź© COMPLETE Last updated: 2026-06-21 Plain-English tagline: TypeScript can be configured to catch many bugs (strict) or few (loose). For new projects, the answer is almost always strict. This is the reasoning, plus when to deviate.


What this decides

In tsconfig.json, you choose how aggressively TypeScript checks your code. The flags cluster into:

  • Strict — "strict": true enables ~10 sub-flags that catch implicit any, possibly-null values, missing return types, etc.
  • Loose — "strict": false (the legacy default) lets implicit any and unsafe nulls slip through.

You can also pick Ă  la carte: "strict": false plus specific flags like "strictNullChecks": true.

For background: TypeScript đźź©.


The short answer

For any new project: "strict": true from day one. No tradeoff worth discussing.

For an existing loose project, migrate incrementally — enable one strict sub-flag at a time, fix the errors it surfaces, ship, repeat.


What “strict” actually enables

"strict": true is shorthand for:

Sub-flagWhat it catches
noImplicitAnyVariables that have no type annotation and no inferred type
strictNullChecksnull and undefined not assignable to non-nullable types
strictFunctionTypesFunction parameter variance correctness
strictBindCallApplyType-checks .bind(), .call(), .apply()
strictPropertyInitializationClass properties must be initialized in constructor
noImplicitThisthis of type any in functions
useUnknownInCatchVariablescatch (e) types e as unknown not any
alwaysStrictParsed source has "use strict" mode

Plus related flags that are technically separate but always-on with strict in practice:

  • noUncheckedIndexedAccess — array[0] returns T | undefined, not T
  • exactOptionalPropertyTypes — { x?: string } doesn’t mean { x: string | undefined }

The factors that matter

  1. Is this a new codebase or an existing one? New → strict, no cost. Existing → migration burden.
  2. How experienced is the team with TypeScript? Strict produces verbose errors that beginners find intimidating; intermediate+ developers benefit hugely.
  3. What’s the cost of a type-related bug in production? High-stakes apps → strict is non-negotiable. Throwaway scripts → it doesn’t matter.
  4. How much external (untyped) JavaScript do you call? Lots → strict is harder (more any boundaries). Modern TypeScript-first ecosystem → easier.

When to pick STRICT

  • Always for new projects. The cost is zero (you’re typing things anyway); the benefit is enormous (bugs caught at edit time).
  • For any production app. Type-safety prevents whole classes of bugs from shipping.
  • For library code that others will depend on. Strict types ARE the API contract.
  • For codebases that have grown past ~5 contributors. Strict types reduce coordination errors.

tsconfig.json for a new Next.js project:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "preserve",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "paths": { "@/*": ["./*"] }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

When to pick LOOSE

Honest answer: rarely. But valid cases exist:

  • Legacy migrations. You inherit a 50,000-line .js project. Going strict on day one means thousands of errors. Start loose; tighten one flag at a time.
  • Rapid prototype that won’t ship. A throwaway script doesn’t need the discipline.
  • Heavy interop with untyped libraries. Some niche libraries lack types and aren’t worth typing. Loose lets the rest of the code stay reasonable.

Even in these cases, enable strictNullChecks as a baseline. Null-safety bugs are the most catastrophic class; getting that one flag pays off even in loose codebases.


The migration path (loose → strict)

Recommended order, one flag at a time:

  1. strictNullChecks — biggest payoff. Surfaces possibly-null/undefined usages.
  2. noImplicitAny — surfaces variables that escaped the type system.
  3. strictFunctionTypes — function-variance correctness; usually few errors.
  4. strictBindCallApply — niche; few errors.
  5. strictPropertyInitialization — only if you use classes; flag each.
  6. noImplicitThis — surfaces methods that lost their this.
  7. useUnknownInCatchVariables — small surface; quick fix.
  8. noUncheckedIndexedAccess — separately from strict. High payoff for code that uses arrays/maps heavily.

For each flag:

  1. Enable in tsconfig.json.
  2. Run npx tsc --noEmit. Get the error count.
  3. Fix errors one at a time (or with a script).
  4. Ship.
  5. Move to the next flag.

For codebases that are too large to fix all at once, use // @ts-expect-error on the failing lines, leave a TODO, ship the flag enabled. Future cleanup work is bounded.


What if I’ve already chosen?

“I started loose and want to go strict”: follow the migration order above. Don’t try to flip everything at once on a big codebase.

“I started strict and find it slowing me down”: read the errors carefully. The slowdown is usually a sign you’re missing type information you should have. Adding the types is the right answer, not loosening the config.

“I have // @ts-ignore sprinkled everywhere”: convert them to // @ts-expect-error. The expect-error variant fails the build if the code on the next line was actually fine — preventing comments from rotting into liars.

“My library types are wrong”: check if a community type fix exists (@types/X). If not, write a custom .d.ts to override locally — better than turning off strict for the whole project.


See also


Sources