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:
- Incomplete context: The LLM saw catch blocks in existing DAOs and generated similar patterns.
- 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.
- Import management: When legacy imports were removed, the fallback code remainedโorphaned and broken.
- 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:
- Type system ambiguity: "No fallback" could mean
null,undefined, or omit the parameter. - JavaScript flexibility: In JavaScript,
nullandundefinedare often interchangeable. - 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.
- Type signature not enforced: The interface expected
undefined, but the code usednull.
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:
-
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 optionallyuser_id(if mapped). - Root Cause: The LLM used generic OAuth2 assumptions, not the specific provider's implementation.
- Generated code assumed the auth token provides common fields like
-
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.
-
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 (
nullvs.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: truesilenced 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:
- Bulk refactoring introduced type errors.
- Developer: "These are type issues, not runtime issues."
- Developer: "We need time to fix the auth payload properly."
- Decision: "Suppress errors temporarily, fix later."
- 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:
- Discovery:
npx tsc --noEmitrevealed the hidden errors. - Categorization: Grouped by error type and root cause.
- Fix Strategy: Addressed each category systematically
- DAO Layer: Remove orphaned fallback code.
- Type Mismatches: Replace
nullwith proper function signatures. - Schema Issues: Add missing properties, fix auth assumptions.
- Verification: Re-enabled
ignoreBuildErrors: falseand verified build. - Result: 0 TypeScript errors, full type safety restored.
Recommendations for LLM Code Generation
For Code Generators
- Include type checking in the loop: Don't assume "it compiles somewhere."
- Ask for context: "What's the schema for this DAO?"
- Validate assumptions: "Auth provider provides these fields: [list]."
- Separate legacy code: Mark, don't delete, unreachable fallbacks.
For Developers Using LLM Code
- Never suppress TypeScript errors to merge code.
- Review generated code for:
- Orphaned error handlers.
- Fallback code for removed dependencies.
- Type mismatches between interfaces and usage.
- Run type checking before builds.
- Ask the LLM: "What assumptions are you making?"
For Team Leads
- Enforce strict TypeScript:
noImplicitAny: true,strict: true. - Add type checking to CI/CD.
- Review
ignoresettings monthly. - 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:
- Incomplete refactoring (legacy removal left orphaned code).
- Type system evasion (
ignoreBuildErrors: truecreated false confidence). - Pattern matching over understanding (LLM saw "try/catch with fallback" and replicated it).
- 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: falseis 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.