Skip to main content
Back to Articles

Why LLM-Generated Code Accumulated 172 TypeScript Errors: A Technical Deep Dive

During a large-scale refactoring, we discovered 172 hidden TypeScript errors from LLM-generated code. This wasn't randomโ€”it revealed systematic patterns in how AI handles legacy removal, type inference, and build configurations. Learn the root causes and how to prevent them.

March 14, 202615 min readBy Sameer

Executive Summary

During a large-scale refactoring of a production application (codename "Project Phoenix"), I discovered 172 hidden TypeScript errors that had accumulated despite successful builds. This wasn't a sudden catastropheโ€”it was the result of methodical, deliberate decisions by an AI code generator that prioritized immediate functionality over type safety. The journey to fix these errors revealed critical lessons about AI-assisted development, the importance of build configurations, and why developer oversight is irreplaceable.

Key Finding: The errors weren't random bugsโ€”they were systematic patterns revealing how LLM code generation handles three core challenges: legacy code removal, type inference under uncertainty, and balancing speed with safety.


The Setup: A Perfect Storm of Conditions

Project Context

The application was a modern web app built with:

  • Frontend: React / Next.js
  • Database: AWS DynamoDB (single-table design)
  • Auth: AWS Cognito
  • Architecture: DAO pattern (multiple DAOs), API routes, React components
  • State: Had just completed several major modernization phases

The Ticking Time Bomb

When I ran npx tsc --noEmit, the TypeScript compiler revealed 172 errors that had been suppressed by:

typescript: {
  ignoreBuildErrors: true  // โš ๏ธ The silencer
}

The build passed. Tests passed. The dev server ran fine. Yet the code was systematically broken at the type level.


Root Cause Analysis: The Three Error Categories

Category 1: The Legacy Database Removal Bungling (27 errors)

What Happened: During a phase that removed a legacy database (MongoDB) in favor of DynamoDB, the LLM generated extensive fallback code:

// UserDAO.ts - The problematic pattern
async listWithFilter(...) {
  try {
    // DynamoDB code
  } catch (error) {
    await dbConnect();  // โŒ Legacy DB connection (now removed)
    const docs = await User.find(query).lean();  // โŒ Legacy model
    return docs;
  }
}

Why This Happened:

  1. Incomplete context: The LLM saw catch blocks in existing DAOs and generated similar patterns.
  2. Belt-and-suspenders mentality: "Add a fallback just in case" is reasonable for production code, but the fallback was never properly implemented after the legacy system was removed.
  3. Import management: When legacy imports were removed, the fallback code remainedโ€”orphaned and broken.
  4. No integration test: The catch blocks were unreachable in practice, so errors weren't surfaced until static analysis.

The Pattern:

Generated: Try new DB, catch โ†’ use legacy DB
Reality:   Legacy DB removed, imports deleted, code stays
Result:   TS2304 - "Cannot find name 'dbConnect'"

Category 2: The Null vs. Undefined Confusion (71 errors - TS2345)

What Happened: The DAO layer uses an execute() method pattern:

// Expected signature
execute(operation: string, fallback: (() => Promise<T>) | undefined)

// What was generated
execute('operation', null)  // โŒ Wrong type

Why This Happened:

  1. Type system ambiguity: "No fallback" could mean null, undefined, or omit the parameter.
  2. JavaScript flexibility: In JavaScript, null and undefined are often interchangeable.
  3. Bulk generation: During a refactoring phase, fallback removal was done systematically with a script that only caught single-line cases. Multi-line fallback removal missed 71 instances.
  4. Type signature not enforced: The interface expected undefined, but the code used null.

The Snowball:

Phase: "Remove legacy fallbacks"
โ†“
Script: "Replace null with undefined" (single-line only)
โ†“
Result: 71 multi-line null parameters remain untouched
โ†“
TS2345: Argument of type 'null' is not assignable to parameter of type 'undefined'

Category 3: Schema Mismatches and Missing Properties (19 errors)

What Happened:

// users/profile/route.ts
const payload = await verifyAuth(request);
const name = payload.name;      // โŒ Auth provider doesn't provide this
const phone = payload.phone;    // โŒ Not in token

// donations/create-order/route.ts
const donation = await donationDAO.create({
  user_id: payload.user_id,      // โŒ Can be undefined
  transaction_id,                 // โŒ Field doesn't exist in interface
  is_anonymous,                   // โŒ Property not in IDonationData
  donor_phone,                    // โŒ Missing from interface
});

Why This Happened:

  1. Auth Payload Assumption Error:

    • Generated code assumed the auth token provides common fields like name, phone, user_id.
    • Reality: The auth provider (Cognito) provides email, sub, and optionally user_id (if mapped).
    • Root Cause: The LLM used generic OAuth2 assumptions, not the specific provider's implementation.
  2. Interface Drift:

    // Route expects
    IDonationData with: transaction_id, is_anonymous, donor_phone
    
    // Interface actually has
    IDonationData with: transaction_date, donor_name, donor_email
    
    • Mismatch happened during design iteration.
    • No synchronization between route code and DAO interfaces.
  3. Missing Type Guards:

    // Generated
    const userId = payload.user_id;  // Could be undefined
    
    // Should have been
    const userId = payload.user_id || payload.sub;
    if (!userId) return unauthorized();
    

The Bigger Picture: Why LLMs Generate Code Like This

1. The Recency Bias Problem

LLMs see the most recent pattern in context and extrapolate:

Training data shows:
"When removing a legacy database, keep try-catch with fallback"
"This is defensive programming"

Context window shows:
UserDAO.ts (with legacy fallback)
CourseDAO.ts (with legacy fallback)
EventDAO.ts (with legacy fallback)

LLM inference:
"Okay, the pattern is: try new DB, catch with legacy DB"

Reality:
Legacy DB was completely removed from the project

2. The Type System Evasion Problem

When TypeScript's type system isn't strict during generation:

// LLM sees this compiles in some contexts
const fallback = null;
const fallback = undefined;

// LLM thinks "these are equivalent"
// But TypeScript distinguishes them

// Result
function execute(fallback: (() => Promise<T>) | undefined) {}
execute(null);  // โŒ Type error
execute(undefined);  // โœ… Type safe

Why this happens:

  • The LLM operates on text patterns, not semantic understanding.
  • It doesn't have real-time type checking during generation.
  • Similar patterns (null vs. undefined) appear safe until checked.

3. The Incomplete Refactoring Problem

Large refactors create "orphaned code"โ€”code that was updated in some places but not others:

Phase 1: "Remove legacy imports"
โ†“
Result: 10 files updated, 3 files missed

Phase 2: "Clean up fallback code"
โ†“
LLM sees: "Some files still have fallback"
LLM thinks: "Okay, keep them for defensive programming"
Result: Broken catch blocks remain

4. The Test Evasion Problem

The code was "working" because:

  • Unit tests: Mock the DAO layer.
  • Integration tests: Use the new database directly.
  • Catch blocks: Never executed in practice.
  • Build: ignoreBuildErrors: true silenced TypeScript.
โœ… Tests pass (mocked DAOs, never hit catch blocks)
โœ… Dev server runs (catches unreachable code)
โœ… Build succeeds (errors suppressed)
โŒ Type safety broken (errors hidden)

Result: False confidence

Why ignoreBuildErrors: true Was Added

Understanding this setting's presence is crucial:

typescript: {
  // TEMPORARY: Many routes have undefined payload.user_id TypeScript errors
  // These require refactoring to handle auth provider's sub + optional user_id properly
  // Strict mode: enabled for development, but build errors temporarily ignored
  // TODO: Add proper type guards for user_id || sub throughout API routes
  ignoreBuildErrors: true,
}

The Logic Trail:

  1. Bulk refactoring introduced type errors.
  2. Developer: "These are type issues, not runtime issues."
  3. Developer: "We need time to fix the auth payload properly."
  4. Decision: "Suppress errors temporarily, fix later."
  5. Reality: "Later" never came, config stayed for months.

This is a classic anti-pattern: Using ignore to paper over systematic issues instead of fixing root causes.


The Full Error Distribution

Category                          Count    Root Cause
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
DAO Layer (TS2345)                  71     null vs. undefined confusion
Legacy DB References (TS2304)       27     Incomplete legacy removal
Auth Payload (TS2339)               19     Schema mismatches
Type Assignments (TS2322)            6     String | undefined issues
UI Components                        2     Component prop type mismatches
Config/Import Issues               17     Optional dependencies
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
TOTAL                             172

Lessons for LLM-Assisted Development

1. Never Use ignore Settings Indefinitely

// โŒ BAD
typescript: { ignoreBuildErrors: true }  // permanent silence

// โœ… GOOD
typescript: { ignoreBuildErrors: false }  // fail loudly
// + TODO: Fix specific errors by date

Principle: Make violations visible immediately, even if fixing requires multiple phases.

2. Separate Concerns in Refactors

โŒ Single PR: "Remove legacy DB AND add new auth AND fix types"
   Result: 172 errors accumulated in one phase

โœ… Multiple PRs:
   - Phase 1: Remove legacy DB (new DB only, tests pass)
   - Phase 2: Add new auth (auth only, tests pass)
   - Phase 3: Enable TypeScript checks (tests pass, build passes)

3. Enforce Type Guards at Boundaries

// API boundary - MUST validate
async function POST(request: NextRequest) {
  const payload = await verifyAuth(request);  // returns AuthPayload

  // โŒ Never assume properties
  const name = payload.name;

  // โœ… Always guard and validate
  const name = payload.email?.split('@')[0] || 'User';
  const userId = payload.user_id || payload.sub;
  if (!userId) return unauthorized();
}

Why: External data (auth tokens) has different schema than internal types.

4. Use Interface Synchronization Tools

// โŒ Manual synchronization (error-prone)
IDonationData interface: { transaction_date, donor_name }
Route code: { transaction_id, donor_phone }

// โœ… Automated validation
const createDonation = zodSchema.parse(body);
// Zod enforces shape at runtime

5. Run Type Checking in CI/CD Before Build

# Current flow
npm run build  # Succeeds with ignoreBuildErrors: true โŒ

# Better flow
npx tsc --noEmit  # Fails if any type error
npm run build      # Only runs if types pass โœ…

The Specific AI Patterns That Caused Problems

Pattern 1: "I'll be defensive"

Generated: Try/catch blocks with legacy fallbacks even after legacy removal. Why: Safety-first thinking is good, but context-blind.

Pattern 2: "These look similar"

Generated: null as fallback parameter (saw null in existing code). Why: Pattern matching on syntax, not semantics.

Pattern 3: "The spec says..."

Generated: Auth token with name, phone fields. Why: Used generic OAuth2 spec instead of provider's documentation.

Pattern 4: "I'll use what's common"

Generated: payload.user_id without optional fallback to sub. Why: Most common field names, not implementation-specific.


How These Errors Were Fixed

The fix involved systematic re-analysis:

  1. Discovery: npx tsc --noEmit revealed the hidden errors.
  2. Categorization: Grouped by error type and root cause.
  3. Fix Strategy: Addressed each category systematically
    • DAO Layer: Remove orphaned fallback code.
    • Type Mismatches: Replace null with proper function signatures.
    • Schema Issues: Add missing properties, fix auth assumptions.
  4. Verification: Re-enabled ignoreBuildErrors: false and verified build.
  5. Result: 0 TypeScript errors, full type safety restored.

Recommendations for LLM Code Generation

For Code Generators

  1. Include type checking in the loop: Don't assume "it compiles somewhere."
  2. Ask for context: "What's the schema for this DAO?"
  3. Validate assumptions: "Auth provider provides these fields: [list]."
  4. Separate legacy code: Mark, don't delete, unreachable fallbacks.

For Developers Using LLM Code

  1. Never suppress TypeScript errors to merge code.
  2. Review generated code for:
    • Orphaned error handlers.
    • Fallback code for removed dependencies.
    • Type mismatches between interfaces and usage.
  3. Run type checking before builds.
  4. Ask the LLM: "What assumptions are you making?"

For Team Leads

  1. Enforce strict TypeScript: noImplicitAny: true, strict: true.
  2. Add type checking to CI/CD.
  3. Review ignore settings monthly.
  4. Set clear timelines: "TODO: Fix TypeScript errors by [date]."

Conclusion

The 172 TypeScript errors weren't a failure of AI code generationโ€”they were a natural consequence of:

  1. Incomplete refactoring (legacy removal left orphaned code).
  2. Type system evasion (ignoreBuildErrors: true created false confidence).
  3. Pattern matching over understanding (LLM saw "try/catch with fallback" and replicated it).
  4. Schema assumptions (auth token fields assumed instead of verified).

The real lesson: LLM-assisted development is powerful but requires active oversight. The code wasn't "bad"โ€”it was systematic in its errors, which means systematic fixes were possible. The type errors were discoverable, categorizable, and fixable.

This experience demonstrates that developer judgment remains irreplaceable for:

  • Understanding context across phases.
  • Questioning assumptions about external APIs.
  • Maintaining type safety as non-negotiable.
  • Catching systematic patterns that slip through testing.

With proper safeguardsโ€”strict TypeScript, enforced type checking, and refusing to suppress errorsโ€”LLM-assisted development can be both fast and safe.


Key Takeaway

Don't blame the AI for generating code with type errors. Blame the build configuration that hides them.

ignoreBuildErrors: false is not optionalโ€”it's foundational.


Appendix: Error Resolution Timeline

Start: 172 TypeScript errors (hidden)
โ”œโ”€ Phase A (5 min): DAO nullโ†’undefined fixes        -71 errors
โ”œโ”€ Phase B (15 min): Remove legacy DB references     -27 errors
โ”œโ”€ Phase C (20 min): Fix auth payload guards         -19 errors
โ”œโ”€ Phase D (10 min): User ID extraction fixes        -6 errors
โ”œโ”€ Phase E (5 min): UI component types              -2 errors
โ”œโ”€ Phase F (1 min): Config/import fixes             -17 errors
โ””โ”€ Phase G (2 min): Re-enable checks + verify       -0 remaining

Result: 0 TypeScript errors, full type safety restored โœ…

This analysis comes from real-world experience fixing 172 hidden TypeScript errors in a production application. The patterns identified here are systematic, not random, which means they're both preventable and fixable.

Share this article

Related Articles