Skip to content

ADR-0003: TypeScript Strict Mode Configuration

Last Updated: 2026-01-08 Status: Active Context: Decksmith


Context

TypeScript is already chosen as the primary language for Decksmith. However, how we configure TypeScript determines how effective it is at catching bugs.

The key questions:

  • Should we enable strict mode?
  • What compiler options should be standard across all packages?
  • How do we structure tsconfig.json files in a monorepo?
  • How strict should we be about type checking dependencies?

This decision affects the entire codebase and is difficult to change later (tightening strictness requires fixing many errors).

Current Decision

We will use TypeScript strict mode with the following configuration strategy:

  1. Base tsconfig.json at repository root with strict mode enabled
  2. All packages extend the base config with environment-specific overrides (DOM for web, Node for backend)
  3. Enable all strict flags including noUncheckedIndexedAccess
  4. Use moduleResolution: "bundler" for modern tooling (Vite, esbuild, Rollup)
  5. Set skipLibCheck: true for flexibility with third-party dependencies
  6. Enable project references for incremental builds and better IDE performance

Key compiler options:

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "bundler",
    "skipLibCheck": true,
    "verbatimModuleSyntax": true
  }
}

Rationale

Why Strict Mode

"Clarity over cleverness" — TypeScript strict mode makes implicit behavior explicit:

  1. strict: true: Enables all strict type-checking options:

    • noImplicitAny: Forces explicit typing (no any by default)
    • strictNullChecks: null and undefined are not assignable to other types
    • strictFunctionTypes: Functions are contravariant in parameters
    • strictBindCallApply: Type-check .bind(), .call(), .apply()
    • strictPropertyInitialization: Class properties must be initialized
    • noImplicitThis: this must have explicit type
    • alwaysStrict: Emit "use strict" in JavaScript output
  2. noUncheckedIndexedAccess: true: Array/object access returns T | undefined

    typescript
    const arr = [1, 2, 3];
    const x = arr[10]; // Type: number | undefined (not just number)

    This prevents the #1 source of runtime errors in TypeScript: assuming array indices are valid.

  3. noImplicitOverride: true: Requires override keyword when overriding base class methods

    • Prevents accidental overrides when base class changes
  4. noFallthroughCasesInSwitch: true: Catches missing break statements in switch cases

Why moduleResolution: "bundler"

Modern bundlers (Vite, esbuild, Rollup) support:

  • ESM and CJS imports interchangeably
  • Extensionless imports (import { foo } from './foo' resolves to foo.ts)
  • Package exports field resolution

"bundler" mode matches this behavior, while "node16" requires explicit extensions (.js), which is verbose for TypeScript projects.

Why skipLibCheck: true

Pragmatic flexibility: Many third-party libraries have minor type errors or depend on old TypeScript versions. Checking library types can cause:

  • Build failures from dependencies we don't control
  • Slower type-checking
  • Maintenance burden (waiting for library updates)

Trade-off: We won't catch type errors in dependencies, but we gain:

  • Faster builds
  • Fewer blockers from third-party type issues
  • More flexibility to use the npm ecosystem

Our code is still strictly checkedskipLibCheck only affects node_modules, not our source code.

Why verbatimModuleSyntax: true

Forces explicit import type and export type for type-only imports:

typescript
import type { User } from './types'; // Type-only import
import { getUser } from './api'; // Value import

Benefits:

  • Better tree-shaking (bundlers can remove type-only imports)
  • Clearer intent (explicit about types vs values)
  • Prevents runtime imports of types

Why Base Config + Extensions

Monorepo structure:

/tsconfig.json (base)
/apps/web/tsconfig.json (extends base, adds DOM)
/apps/api/tsconfig.json (extends base, adds Node types)
/packages/domain/tsconfig.json (extends base, minimal)

Benefits:

  • Consistency: All packages have same strict settings
  • DRY: No duplication of compiler options
  • Easy updates: Change base config once, all packages inherit
  • Environment-specific: Each package can add DOM or Node types as needed

Trade-offs

Benefits:

  • Fewer runtime errors: Strict mode catches entire classes of bugs at compile time
  • Better autocomplete: Explicit types improve IDE suggestions
  • Easier refactoring: TypeScript can track changes across the codebase
  • Consistent standards: All packages use same strict settings
  • Self-documenting code: Explicit types serve as inline documentation
  • Faster builds: skipLibCheck: true reduces type-checking time

Costs:

  • More verbose code: Must explicitly type parameters, return values, nullability
  • Longer compile times: Strict checks take more time (mitigated by incremental builds and skipLibCheck)
  • Steeper learning curve: New contributors must understand strict TypeScript
  • No dependency type checking: Won't catch type errors in third-party libraries

Risks:

  • False sense of security: TypeScript guarantees are only at compile time. Runtime validation (Zod) is still needed.
    • Mitigation: ADR-future will define validation strategy (Zod at boundaries)
  • Overly strict for prototyping: Strict mode can slow down experimentation
    • Mitigation: For quick prototypes, use @ts-expect-error with a comment explaining why
  • Breaking changes when tightening: If we start with loose config, tightening later requires fixing many errors
    • Mitigation: Start strict from day 1 (this ADR)

Evolution History

2026-01-08: Initial decision

  • Enabled all strict mode flags
  • Chose moduleResolution: "bundler" for modern tooling
  • Set skipLibCheck: true for pragmatic flexibility with dependencies
  • Established base config + extensions pattern

References

Built with VitePress