TypeScript

198 fragments · Layer 3 Synthesized established · 198 evidence · updated
↓ MD ↓ PDF

eliminate-14-unjustified-any.md
- wiki/engineering/typescript/labelcheck-48c1201-fix-all-typescript-compilation-errors---achieve-0.md
- wiki/engineering/typescript/stride-v2-345125e-comprehensive-typescript-and-eslint-cleanup.md
- wiki/engineering/typescript/hazardos-20ce7c2-fix-lint-errors-and-improve-typescript-configurati.md
- wiki/engineering/typescript/stride-v2-f8bd428-resolve-code-quality-and-security-linting-warnings.md
- wiki/engineering/typescript/asymxray-023f6f5-resolve-typescript-and-eslint-errors.md
- wiki/engineering/typescript/eydn-app-e451de6-fix-all-ci-lint-errors-eliminate-bare-any-types-an.md
- wiki/engineering/typescript/orbitabm-26e9b64-add-vitest-infrastructure-and-34-integration-tests.md
- wiki/engineering/typescript/stride-v2-fa54624-lazy-database-initialization-and-security-lint-con.md
first_seen: "unknown"
last_updated: "2025-07-14"
hypothesis: false
fragment_count: 198
tags: [typescript, eslint, testing, zod, type-safety, linting]


Summary

any is the primary source of TypeScript pain across all projects — not because it causes immediate errors, but because it defers them to runtime and makes refactors invisible to the compiler. The consistent pattern across 13 projects is that any accumulates silently until a systematic cleanup pass is forced by a broken build or a production incident. Replacing any with unknown in catch blocks and context parameters, and using z.infer<typeof schema> for Zod-derived types, eliminates the majority of unsafe casts without requiring complex type gymnastics. ES module hoisting and test-file type conflicts are the two structural issues that cause the most confusing failures — both have reliable fixes (dynamic imports and a separate tsconfig.test.json) that are worth applying proactively rather than reactively.

TL;DR

What we've learned
- any accumulates to 100+ instances before anyone notices; a centralized type system and a lint rule enforcing no-explicit-any are the only reliable prevention [1]
- ES module imports are hoisted before env vars load — dynamic imports are the fix, not dotenv placement [2]
- Test files and production code need separate tsconfig files; intentionally partial mocks break strict checking in the main config [3]
- Zod's ZodError changed .errors to .issues in a minor version; pin Zod as a direct dependency [4]
- Database schema drift is the most common source of runtime errors that TypeScript should have caught — interfaces must be regenerated whenever migrations run [5]

External insights

No external sources ingested yet for this topic.


Common Failure Modes

1. any accumulation until the build breaks

The failure isn't a single any — it's 50–146 of them, added one at a time to silence a compiler error, until a strict-mode upgrade or a new lint rule surfaces them all at once. In LabelCheck, a single cleanup pass replaced 146 instances of any with proper types [1]; in AsymXray, a separate pass eliminated 14 unjustified as any assertions by introducing backward-compatibility helpers [6].

The fix is structural, not per-instance. Create a centralized types/ directory, enforce @typescript-eslint/no-explicit-any in ESLint, and use unknown as the default for catch blocks and context parameters:

// Before — defers the problem
catch (e: any) {
  console.error(e.message);
}

// After — forces you to handle it
catch (e: unknown) {
  const message = e instanceof Error ? e.message : String(e);
  console.error(message);
}

Consistent across projects: AsymXray, LabelCheck, Hazardos, Eydn, Stride v2.


2. ES module hoisting breaks env var loading in test scripts

Static import statements are hoisted to the top of the module before any runtime code executes — including dotenv.config() calls. The symptom is that environment variables are undefined inside imported modules even though dotenv is called first in the file.

Error: DATABASE_URL is not defined

The root cause: by the time dotenv.config() runs, the imported module has already evaluated and captured undefined from process.env.

Fix: use dynamic imports after env loading:

// Broken — import is hoisted above dotenv.config()
import { db } from './db';
dotenv.config();

// Fixed — db module loads after env vars are set
dotenv.config();
const { db } = await import('./db');

Observed in Stride v2 test scripts [2]. The lazy-loaded singleton pattern (instantiate on first access, not at module load) solves the same problem for production code [7].


3. Test mocks fail type checking because they're incomplete

When an interface has 12 required fields and a test mock only provides 4, TypeScript strict mode rejects the mock. The natural response is as any — which defeats the purpose of the test. The correct responses are Partial<T> for genuinely incomplete mocks, or filling in the required fields with typed stubs.

// Broken — silences the error, hides future drift
const mockUser = { id: '123', email: 'test@test.com' } as any;

// Fixed — compiler enforces shape
const mockUser: Partial<User> = { id: '123', email: 'test@test.com' };

// Or, for full mock with typed stubs
const mockUser: User = {
  id: '123',
  email: 'test@test.com',
  createdAt: new Date(),
  // ... all required fields
};

Test files also need jsdom environment directives for DOM assertions and proper vitest imports (vi, describe, it, expect, beforeEach) — missing these causes confusing TypeScript errors that look like type failures but are actually missing globals.

Consistent across projects: AsymXray, LabelCheck, Hazardos, Stride v2 [8].


4. Database schema drift between TypeScript interfaces and actual columns

TypeScript interfaces written by hand against a Supabase schema go stale when migrations run. The compiler doesn't know the schema changed — it only knows the interface. The result is runtime errors that look like data bugs but are actually type bugs.

TypeError: Cannot read properties of undefined (reading 'name')
// The column was renamed in a migration but the interface still uses the old name

Fix: regenerate types from the database after every migration using supabase gen types typescript. For Supabase, the generated types include Row, Insert, and Update variants — use them instead of hand-written interfaces.

Observed in Hazardos (schema/interface drift causing runtime errors) [5] and AsymXray (cron job schema issues) [9].


5. process.env.NODE_ENV is read-only in strict mode

TypeScript marks process.env.NODE_ENV as string | undefined, but the Node.js type definitions make it effectively read-only in strict configurations. Test files that try to override it directly get a compilation error.

// Fails in strict mode
process.env.NODE_ENV = 'test';

// Fix — type assertion required
(process.env as { NODE_ENV: string }).NODE_ENV = 'test';

Observed in LabelCheck test files [10].


6. ZodError API changed between versions (.errors.issues)

In Zod v3, the property for accessing validation errors changed from .errors to .issues. Code written against the old API compiles fine if Zod is a transitive dependency at the old version, then breaks silently when the transitive version bumps.

// Old API — breaks in Zod v3+
if (error instanceof ZodError) {
  console.log(error.errors);
}

// Current API
if (error instanceof ZodError) {
  console.log(error.issues);
}

Also: ZodError construction requires a code property — instantiating it without one causes a runtime error, not a compile error.

Fix: pin Zod as a direct dependency in package.json rather than relying on it as a transitive dep from another library [11]. Observed in Hazardos production build [12].


7. Function declarations inside blocks rejected by strict mode

TypeScript strict mode (and ES strict mode) prohibits function declarations inside if blocks, switch cases, or other block scopes. This is a valid JavaScript pattern in sloppy mode but a compile error in strict.

// Fails in strict mode
if (condition) {
  function handleIt() { ... } // Error: Function declarations not allowed here
}

// Fix — use const arrow function
if (condition) {
  const handleIt = () => { ... };
}

Observed in LabelCheck [13].


8. PascalCase imports break after kebab-case file renames

When a file is renamed from HazardsSection.tsx to hazards-section.tsx (kebab-case convention), existing imports using the PascalCase path break on case-sensitive filesystems (Linux/CI) even though they work locally on macOS (case-insensitive).

// Breaks on Linux CI after rename
import { HazardsSection } from './HazardsSection';

// Fix
import { HazardsSection } from './hazards-section';

Observed in Hazardos after a naming consistency refactor [14].


What Works

Separate tsconfig files for tests and production

Excluding test directories from the main tsconfig.json prevents intentionally partial mocks and test-only patterns from polluting the production type check. A tsconfig.test.json that extends the main config but includes test directories gives tests their own type environment.

// tsconfig.test.json
{
  "extends": "./tsconfig.json",
  "include": ["**/*.test.ts", "**/*.spec.ts", "__tests__/**/*"],
  "compilerOptions": {
    "strict": true,
    "types": ["jest", "node"]
  }
}

Validated in Hazardos and Stride v2 [15].


Zod for API validation with z.infer<typeof schema> for types

Defining a Zod schema and deriving the TypeScript type from it (rather than writing both separately) keeps the runtime validation and the compile-time type in sync automatically. When the schema changes, the type changes.

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
  role: z.enum(['admin', 'user']),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;
// No separate interface needed — type is derived from schema

Use z.infer<typeof schema> — not schema._type, which is an internal property and not part of the public API [16]. Validated in LabelCheck, Hazardos, Orbit ABM [17].


Lazy-loaded singleton for database connections

Instantiating a database client at module load time causes build failures when DATABASE_URL isn't set (e.g., during Next.js static generation). A lazy proxy that initializes on first access avoids this:

let _db: ReturnType<typeof createClient> | null = null;

function getDb() {
  if (!_db) {
    _db = createClient(process.env.DATABASE_URL!);
  }
  return _db;
}

Validated in Stride v2 [18].


unknown instead of any for context parameters and catch blocks

unknown forces the caller to narrow the type before using it, which is the correct behavior for values that genuinely could be anything. any skips narrowing entirely. The migration path is mechanical: replace any with unknown, then fix the resulting errors by adding type guards or instanceof checks.

Validated in AsymXray (withMetrics context parameter) and Stride v2 [19].


Pre-push quality gate: tsc --noEmit && eslint && next build

Running TypeScript compilation, lint, and a production build before push catches the class of errors that only surface in CI — particularly any accumulation, unused imports that become errors in strict mode, and build-time env var issues. Validated in Hazardos and Orbit ABM [20].


Extracting route handlers into service modules

Route files that grow past ~150 lines become hard to test and type-check because they mix HTTP concerns (request parsing, response formatting) with business logic. Extracting the business logic into a dedicated service module reduces route file size by 42–93% and makes the service independently testable.

Validated in LabelCheck (42% reduction) and AsymXray [21].


Gotchas and Edge Cases

Timezone-sensitive date tests fail on CI but pass locally. new Date('2024-01-15') parses as UTC midnight, which becomes the previous day in negative-offset timezones. Use new Date(2024, 0, 15) (local time constructor) in tests. Observed in AsymXray [22].

Array.from() required for .entries() on certain iterables. TypeScript's downlevelIteration setting affects whether spread and for...of work on iterables. When it's off, .entries() on a Map or NodeList requires Array.from() to avoid iterator errors. Observed in LabelCheck [23].

React.cloneElement requires explicit type casting. The return type of React.cloneElement is ReactElement<any>, which doesn't satisfy stricter prop types. Explicit casting is required: React.cloneElement(child) as React.ReactElement<SpecificProps>. Observed in Stride v2 and Hazardos [24].

eslint-plugin-security produces high false-positive rates in React/Next.js codebases. The plugin flags object[variable] property access as a potential injection vulnerability, which fires constantly on normal React patterns. Tuning the config to disable the highest-noise rules reduces warnings by 80–90% without sacrificing real security coverage. Observed in Stride v2 [25].

require() in TypeScript files triggers ESLint @typescript-eslint/no-require-imports. The fix is converting to ES6 import syntax, not adding a disable comment. Disable comments for non-existent plugin rules also cause their own lint errors. Observed in Hazardos and Stride v2 [26].

Database null values don't automatically become TypeScript undefined. Supabase returns null for nullable columns; TypeScript function parameters typed as T | undefined don't accept null. Explicit conversion (value ?? undefined) is required at the boundary. Observed in AsymXray [27].

Discriminated unions require conditional checks, not destructuring. Destructuring a discriminated union before narrowing it causes a type error because TypeScript can't guarantee which variant you have. Use if (result.type === 'success') before accessing result.data. Observed in LabelCheck [28].

React Server Components reject Date.now() calls. RSC purity rules require that server components be deterministic. Date.now() is a side effect. Assign it to a variable outside the component or pass it as a prop. Observed in Eydn [29].


Where Docs Disagree With Practice

Docs say as any is a last resort; in practice it's a first resort. The TypeScript handbook frames any as an escape hatch for genuinely unknown types. In practice across 13 projects, it's used to silence compiler errors during development and never cleaned up. The only reliable counter-measure is @typescript-eslint/no-explicit-any in ESLint with no exceptions for production code — not documentation or code review.

Zod docs show .errors for accessing validation failures; the current API is .issues. The Zod v3 migration guide mentions this, but the main documentation examples weren't consistently updated. Code copied from older blog posts or the docs will use .errors and fail at runtime without a compile-time warning if Zod's version isn't pinned [12].

ESLint security plugin docs present it as production-ready for Node/React; in practice it requires significant tuning. The eslint-plugin-security README doesn't quantify false-positive rates. In Stride v2, enabling it with default settings produced enough noise to make the output unactionable — 80–90% of warnings were false positives on normal React patterns [25].

TypeScript docs say strict: true enables all strict checks; in practice noUncheckedIndexedAccess is not included. This is technically correct but surprising — array index access (arr[0]) returns T, not T | undefined, even with strict: true. Projects that want index safety need to add noUncheckedIndexedAccess explicitly.

Jest V8 coverage provider is documented as equivalent to the default Babel provider; in practice it resolves module mocking conflicts the default provider doesn't. Observed in Stride v2 when switching providers resolved intermittent mock failures [30].


Tool and Version Notes

Zod: Pin as a direct dependency. The .errors.issues rename happened in v3; ZodError construction requires a code property. Using Zod as a transitive dependency exposes you to silent version drift.

@swc/jest: Faster TypeScript compilation in Jest than ts-jest. Validated in LabelCheck [31]. Worth the setup cost on large test suites.

Vitest with happy-dom: Suitable for testing React components and business logic without a full browser. Validated in AsymXray (430-test scaffold) and Hazardos [32].

@typescript-eslint v6+: The no-unused-vars rule from base ESLint should be disabled in favor of @typescript-eslint/no-unused-vars — running both produces duplicate errors. The underscore prefix convention (_unusedParam) suppresses the rule for intentionally unused parameters.

Next.js App Router + TypeScript: Date.now() in Server Components triggers purity lint errors. API route handlers should be typed as Promise<Response> explicitly — the inferred return type from NextResponse.json() is loose enough to hide mismatches [33].

eslint-plugin-security: Requires tuning before it's useful. Disable security/detect-object-injection and security/detect-non-literal-regexp as a starting point, then re-enable selectively. Default config is too noisy to be actionable [25].



Sources

Synthesized from 198 fragments: git commits across AsymXray, LabelCheck, Hazardos, Stride v2, Eydn, Orbit ABM, ContentCommand, and 6 additional projects (Adava Care, Asymmetric Marketing, BluePoint ATM, Crazy Lenny's, Doudlah Farms, Quarra Stone). No external sources ingested. Date range: unknown to unknown.

Sources

  1. Labelcheck 8F41A56 Create Centralized Type System And Migration Guide
  2. Stride V2 2552883 Use Dynamic Imports In Test Scripts To Avoid Env V
  3. Hazardos 20Ce7C2 Fix Lint Errors And Improve Typescript Configurati
  4. Hazardos 321E1F2 Resolve Typescript Errors For Production Build, Orbitabm B0Db36F Add Zod As Direct Dependency
  5. Hazardos E9940E6 Estimate Auto Generation From Site Surveys And Lin
  6. Labelcheck A86A44F Improve Type Safety Eliminate 14 Unjustified Any
  7. Stride V2 Fa54624 Lazy Database Initialization And Security Lint Con, Stride V2 23Ee55B Fix Test Seeding Infrastructure And Environment Lo
  8. Stride V2 353E3C4 Fix Typescript Errors In Test Files, Hazardos 98Ab5C2 Fix Test File Imports And Type Annotations
  9. Asymxray 96Abf1B Resolve Cron Job Database Schema Issues
  10. Labelcheck D1Ed25A Fix Typescript Errors In Test Files Nodeenv Read
  11. Orbitabm B0Db36F Add Zod As Direct Dependency
  12. Hazardos 321E1F2 Resolve Typescript Errors For Production Build
  13. Labelcheck Face57D Fix Typescript Build Error Convert Function Declar
  14. Hazardos E7Eca2D Update Hazards Section Imports To Use Barrel Expor
  15. Hazardos 20Ce7C2 Fix Lint Errors And Improve Typescript Configurati, Stride V2 E55A631 Complete Typescript And Ci Pipeline Fixes
  16. Hazardos 66E4690 Update Api Routes With Unified Handler Pattern And
  17. Labelcheck 0316D61 Add Centralized Input Validation With Zod Technica, Orbitabm 26E9B64 Add Vitest Infrastructure And 34 Integration Tests
  18. Stride V2 Fa54624 Lazy Database Initialization And Security Lint Con
  19. Asymxray Bb5E2Df Use Unknown Type For Withmetrics Context Parameter, Stride V2 345125E Comprehensive Typescript And Eslint Cleanup
  20. Hazardos D002869 Add Comprehensive Development Rules And Quality Ch, Orbitabm 4F8Eb44 Add Pre Push Quality Checks Rule And Fix Lint Warn
  21. Labelcheck B238F05 Phase 3 Extract Orchestration Logic To Dedicated M, Asymxray Da05243 Extract Remaining Service Layers From Large Api Ro
  22. Asymxray 392E608 Use Explicit Local Dates In Date Range Selector Te
  23. Labelcheck 67D49A0 Fix Vercel Build Errors Update To New Pricing Ti
  24. Stride V2 353E3C4 Fix Typescript Errors In Test Files, Hazardos F5B7Ce2 Add Paymentfailed Notification Type And Fix Sheett
  25. Stride V2 Ff89238 Tune Eslint Security Config To Reduce False Positi
  26. Hazardos 20Ce7C2 Fix Lint Errors And Improve Typescript Configurati, Stride V2 6603C9D Remove Invalid Eslint Disable Comments For Non Exi
  27. Asymxray 088D43E Handle Null Values From Database In Cron Sync
  28. Labelcheck 48C1201 Fix All Typescript Compilation Errors Achieve 0
  29. Eydn App Cdc654A Fix Lint Errors Escaped Entity Impure Datenow Img
  30. Stride V2 29F8226 Resolve Jest Coverage Instrumentation Issues
  31. Labelcheck F07Bc98 Add Comprehensive Testing Infrastructure With 54 U
  32. Asymxray 603F1D5 Add Testing Scaffold With 430 Tests And Infrastruc, Hazardos 7Ee330F Add Documentation And Test Suite
  33. Asymxray 75De98B Add Explicit Return Types To Secret Rotation Route

Fragments (198)