· typescript / strictness / bugs

Does TypeScript strictness actually reduce your bug rate?

TypeScript strict mode catches ~15% of committed bugs — not the 38% from Airbnb. The honest verdict from peer-reviewed data and 3 industry case studies.

By

2,254 words · 12 min read

The most honest number from peer-reviewed research: TypeScript strict mode catches 15% of publicly-committed JavaScript bugs. Not 38%, despite what the Airbnb conference talk says. Not 0%, despite what the skeptics argue. Somewhere in between, with the actual ceiling depending on whether you also enable noUncheckedIndexedAccess — a flag the TypeScript team deliberately excluded from strict.

Enable strict: true. The migration cost is real and front-loaded, but TypeScript 6.0 made it the default in 2026. Go in with accurate expectations, not inflated ones.

Who this is for

You are considering enabling strict: true on an existing TypeScript codebase, evaluating whether the migration pain is worth it, or trying to answer the “does this actually reduce bugs?” question with something more rigorous than vibes. This is not a tutorial on how to enable strict mode — it is a review of the empirical evidence.

If your codebase is already strict, the verdict section has the checklist of what you are still missing.

What strict: true actually enables

strict: true in tsconfig.json is a shorthand for nine compiler flags. As of TypeScript 5.x:

FlagWhat it catches
strictNullChecksnull/undefined not assignable to non-null types; forces explicit handling
noImplicitAnyVariables/params without inferable type must be annotated
strictFunctionTypesFunction parameter types checked contravariantly on assignments
strictPropertyInitializationClass fields must be initialized in constructor or marked !
strictBindCallApply.call(), .bind(), .apply() checked against original signature
noImplicitThisthis in functions must have declared type
useUnknownInCatchVariablescatch (e) gives e: unknown instead of e: any (TS 4.4+)
alwaysStrictEmits "use strict" and parses files in strict ECMAScript mode
strictBuiltinIteratorReturnBuilt-in iterators typed with TReturn = undefined (TS 5.6+)

strictNullChecks and noImplicitAny do most of the bug-prevention work. The rest are genuinely useful but less impactful in practice.

What is deliberately not in strict

Three flags provide material safety gains but are excluded from strict:

noUncheckedIndexedAccess is the most important omission. Array indexing arr[i] and object indexing obj[key] return T | undefined instead of T. This catches the single most common source of Cannot read properties of undefined crashes in TypeScript code. The TypeScript team explicitly declined to include it (GitHub issue #49169, closed as “Declined — doesn’t match the TypeScript vision”) because on existing codebases it generates hundreds of errors and makes migration prohibitively expensive.

exactOptionalPropertyTypes prevents assigning undefined to an optional property foo?: string; you must explicitly type it as foo?: string | undefined. Closes a subtle hole where { foo: undefined } and {} are treated as equivalent.

noUncheckedSideEffectImports (TS 6.0+) catches typo’d side-effect imports.

TypeScript is not trying to be correct

TypeScript’s Design Goals list this as an explicit non-goal:

“Apply a sound or ‘provably correct’ type system. Instead, strike a balance between correctness and productivity.”

TypeScript is deliberately unsound in several ways: structural typing, type assertions, any. This sets a ceiling on what strictness can guarantee.

TypeScript 6.0 (2026) made strict: true the new tsconfig default and deprecated alwaysStrict: false. The signal: strict is no longer “opt-in safety” but table stakes.

What the data says

The peer-reviewed number: 15%

Gao, Bird, and Barr at ICSE 2017 (To Type or Not to Type: Quantifying Detectable Bugs in JavaScript) sampled 400 confirmed bugs from 398 public GitHub JavaScript projects. They manually added type annotations to the pre-fix code and checked whether TypeScript 2.0 reported an error.

Result: TypeScript detected 58 of 400 bugs — 15% (95% CI: 11.5%–18.5%).

Limitations the authors name: only publicly-reported fixed bugs were studied (private bugs not captured); a 10-minute annotation cap per bug; the study used TypeScript 2.0 with default settings, not strict: true. The 15% figure reflects partial annotation effort on a limited sample — treat it as a floor, not a ceiling, for a fully annotated codebase with all strict flags enabled.

Flow 0.30 detected nearly the same number (59/400), with only ~3 bugs exclusive to each tool. The two checkers are largely redundant.

The industry number: 38% (with caveats)

Brie Bunge at Airbnb estimated at JSConf Hawaii 2019 that 38% of Airbnb’s recent bugs would have been preventable with TypeScript. That number has been cited repeatedly since.

The caveats: sample size, time period, and categorization methodology are not disclosed in the talk. The analysis was internal and not peer-reviewed. It predates Airbnb’s mature TypeScript adoption; strict mode may not have been enabled on the analyzed code. The gap between 38% and the academic 15% is large and unexplained — possible explanations include Airbnb counting JS bugs in a partially-typed codebase (a lower bar), different severity filters, and a different definition of “preventable.”

Use 38% as directional motivation, not precise measurement. The rigorous number is 15%.

What a 2022 repo study found — and why it’s complicated

Bogner and Merkel (arXiv:2203.11115) mined 604 GitHub repos — 299 JavaScript, 305 TypeScript — across 16 million lines of code. They measured code quality, complexity, bug-fix commit ratio, and bug resolution time.

The uncomfortable result: TypeScript projects had significantly better code quality (fewer code smells, lower complexity) but a higher bug-fix commit rate — 0.206 versus 0.126 for JavaScript projects.

The authors’ interpretation: “The perceived positive influence of TypeScript for avoiding bugs in comparison to JavaScript may be more complicated than assumed.” Their hypothesis for the discrepancy is confounders — TypeScript projects on GitHub tend to be larger and more complex, which naturally surfaces more tracked bugs.

Two additional caveats: the study does not control for strictness levels (a TypeScript codebase using any heavily is not using type safety), and higher any usage correlated with worse quality metrics (Spearman’s ρ = 0.17–0.26) but not with bug-fix commit rates specifically.

What TypeScript projects actually fail on today

A 2026 taxonomy study (arXiv:2601.21186) examined 633 confirmed bug reports from 16 popular open-source TypeScript repositories. Bug categories by frequency:

  1. Tooling and configuration faults — the dominant category
  2. API misuse
  3. Async / promise handling errors
  4. Logic and syntax errors — reduced compared to JavaScript baselines
  5. Type-related runtime errors — also reduced compared to JavaScript

The key finding: “Modern TypeScript failures often arise at integration and orchestration boundaries rather than within algorithmic logic.” Strict typing has already shifted what TypeScript projects fail on. Type and null errors are rarer. Build systems, async boundaries, and API contracts remain wide open.

What it won’t catch

Runtime data shapes. TypeScript types are erased at compile time. If your API returns { name: null } when your type says { name: string }, TypeScript won’t catch it. Use Zod, Valibot, or io-ts for runtime validation.

Logic errors. Off-by-one, wrong algorithm, wrong business rule — all invisible to the type checker. TypeScript reasons about what types flow through code, not what the code does.

Type assertions. The as keyword bypasses type checking entirely. Strict mode does not restrict as. A codebase making liberal use of as accumulates silent lies to the type system.

Explicit any. noImplicitAny prevents implicit any, not explicit any. You can still write const x: any = ... and lose all type safety. Strict mode does not prevent deliberate any.

Async and promise bugs. TypeScript types promises and async functions correctly but cannot catch uncaught promise rejections at chain boundaries, race conditions, or timing-dependent behavior. The 2026 taxonomy study listed async/promise bugs as the third-largest category in TypeScript projects.

Tooling and build configuration. The single largest bug category in the 2026 taxonomy. Strictness has zero effect on webpack config mistakes, tsconfig path mapping errors, or mismatched @types versions. For the specific build and configuration bugs that hit TypeScript monorepos in production, see Turborepo monorepo pitfalls we learned the hard way.

Index access without noUncheckedIndexedAccess. arr[99] on a 3-element array types as T, not T | undefined, unless you add the flag separately. This is the most common source of Cannot read properties of undefined crashes that strict: true still does not catch.

Here is what the gap looks like in code:

type User = { name: string; email?: string };
const users: Record<string, User> = {
  alice: { name: "Alice", email: "[email protected]" },
};

// WITHOUT strict
function greetLegacy(userId: string) {
  const user = users[userId];   // typed as User — not User | undefined
  return `Hello, ${user.name}`; // compiles cleanly, crashes at runtime for unknown userId
}

function sendEmailLegacy(user: User) {
  return user.email.toUpperCase(); // compiles cleanly, crashes: email is optional
}

// WITH strict: true
function greetStrict(userId: string) {
  const user = users[userId];
  // Still typed as User without noUncheckedIndexedAccess — same crash risk
  // With noUncheckedIndexedAccess: typed as User | undefined ↓
  if (!user) return "Unknown user";
  return `Hello, ${user.name}`; // safe
}

function sendEmailStrict(user: User) {
  // Error TS2532: Object is possibly 'undefined'
  // user.email.toUpperCase() — caught at compile time
  return user.email?.toUpperCase() ?? "(no email)"; // correct pattern
}

// strictFunctionTypes: parameters checked contravariantly
type Handler = (event: MouseEvent) => void;
// Without strictFunctionTypes: (e: Event) => void silently accepted as Handler
// With strictFunctionTypes: compile error — Event is wider than MouseEvent

// noImplicitAny: forces explicit annotation
// function process(data) { return data.value * 2; }  — Error TS7006
function processStrict(data: { value: number }) {
  return data.value * 2;
}

Migration cost

The numbers are real.

OrganizationScopeErrors on migration
Figma~1,162 files, ~376k LoC4,000+ errors (adding strictNullChecks only)
Generic large project4-year-old project~4,300 errors (full strict: true)
Slack Desktop6-month migration”many small bugs” found

Figma’s case (Figma Engineering Blog) is the most detailed primary account: 30 engineers, two 3-day sprints, 4,000+ errors across 1,162 files from enabling strictNullChecks alone. Their conclusion: “A few of our high-severity incidents would have been caught before landing on production” and post-migration “null errors no longer show up in our error dashboard.” No before/after bug-rate statistics were published, but the qualitative signal is positive.

Flag-by-flag beats all-at-once

For large codebases, the recommended migration sequence:

  1. noImplicitAny first — can be done file-by-file with // @ts-check before touching tsconfig.json
  2. strictNullChecks second — generates the most errors but delivers the most value
  3. Remaining strict flags — lower friction, add them together

Turning on strict: true all at once on a 300k-line codebase creates a quarter-long stalemate. Figma’s progressive approach — compiling twice, gating on dependency graph — is documented and reproducible.

Most errors surfaced during migration are real bugs, not false positives. strictPropertyInitialization is the main exception: it generates genuine false positives when initialization happens outside the constructor (Angular lifecycle hooks, dependency injection frameworks). Account for this when triaging.

IDE enforcement closes the feedback gap

JetBrains WebStorm surfaces TypeScript strict errors inline as you type, without a separate compile step. If you are working on a team where not everyone runs tsc --watch continuously, IDE-level enforcement closes the feedback gap — the error appears at the point of writing, not at CI. This matters most during migration when the goal is eliminating the error backlog one file at a time.

Verdict

High confidence — cite these directly:

  • TypeScript strict mode catches a reproducible, peer-reviewed ~15% of publicly-disclosed JavaScript bugs (Gao et al. 2017, n=400).
  • Enabling strictNullChecks on a real codebase surfaces hundreds to thousands of real errors, not false positives (Figma, Slack primary reports).
  • The 2026 taxonomy shows strict typing has already changed what TypeScript projects fail on — type and null errors are rarer; tooling, async, and API misuse are the dominant failure modes now.
  • TypeScript 6.0 made strict: true the default. The industry has decided.

Medium confidence — hedge with caveats:

  • Airbnb’s 38% is plausible but methodologically opaque. Treat it as illustrative motivation, not citable precision.
  • TypeScript projects do not have statistically fewer bug-fix commits than JavaScript projects (Bogner & Merkel 2022), but this result is confounded by project complexity and selection effects.

Honest “we don’t know”:

  • No large-scale controlled study exists comparing strict: true vs strict: false within the same codebase over time.
  • No survey data on what percentage of TypeScript projects actually use strict: true.
  • Long-term production bug-rate changes after migration remain anecdotal.

The call: enable strict: true. Add noUncheckedIndexedAccess separately if you can absorb the initial error volume — it catches the class of bugs that strict still misses. Don’t expect your bug rate to drop by a third; expect a material improvement in null/undefined handling and a type surface that future contributors can actually trust.

If you’re weighing TypeScript against Go for a new backend service, Go vs TypeScript — backend service language pick for 2026 covers the throughput benchmarks and team composition tradeoffs that determine the real 2026 pick.

References

SourceType
Gao et al. 2017 (ICSE) — To Type or Not to TypePeer-reviewed
Bogner & Merkel 2022 — arXiv:2203.11115Preprint
Bugs in TS Ecosystem 2026 — arXiv:2601.21186Preprint
Brie Bunge, JSConf Hawaii 2019 — Adopting TypeScript at ScaleConference talk
Figma — Inside Figma: a case study on strict null checksEngineering blog
TypeScript Design GoalsOfficial docs
TypeScript TSConfig ReferenceOfficial docs
GitHub issue #49169 — noUncheckedIndexedAccess in strictGitHub issue
TypeScript 6.0 Release NotesOfficial docs