Skip to main content

Test Configuration Best Practices Research

Executive Summary

Based on research from TypeScript, ESLint, and Vitest communities, the recommended approach is:

✅ EXCLUDE test files from strict typecheck → Use separate tsconfig for test validation → Relax strictness selectively for test patterns


Key Findings

1. TypeScript Community Standards (Official Docs)

Separation of Concerns

  • TypeScript's official handbook recommends separate tsconfig files for different parts of a project

  • Example pattern:

    // src/tsconfig.json - for production code
    {
    "compilerOptions": { "outDir": "../dist" },
    "exclude": ["**/*.test.ts"]
    }

    // src/tsconfig.test.json - for tests only
    {
    "compilerOptions": { "outDir": "../dist/test" },
    "include": ["**/*.test.ts"],
    "references": [{ "path": "./tsconfig.json" }]
    }

Why Separate Configs Matter

  • Production tsconfig: Strict, no noEmit, emit actual output
  • Test/Lint tsconfig: Can be more lenient, focuses on analysis not emission
  • Prevents: Test-only code affecting production type integrity

Strictness Inheritance Pattern

  • Base config (tsconfig.base.json) sets foundation rules
  • Specific configs override/relax as needed per context
  • Test contexts naturally relax exactOptionalPropertyTypes and similar strict patterns

2. ESLint Community Patterns

Official Pattern: File-Specific Overrides (antfu/eslint-config, Trust Score 10)

From the most authoritative ESLint configuration library:

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu(
{
// Base rules for everything
},
{
// FILE-SPECIFIC OVERRIDE for tests
files: ['**/*.test.ts', '**/__tests__/**'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
// ... test-specific relaxations
},
},
)

Why This Works

  • ESLint supports file-scoped rule overrides in flat config
  • Test files can have different rules than production code
  • No need to exclude tests entirely; instead, tailor the rules

Benefits Over Exclusion

  • Tests are still linted (consistent formatting, naming)
  • But test-specific patterns (mocks, any casts, console logs) are allowed
  • Catches real issues while allowing testing patterns

3. Vitest Best Practices (eslint-plugin-vitest, Trust Score 8.3)

Configuration Pattern for Test Overrides

{
"overrides": [
{
"files": ["test/**"],
"plugins": ["vitest"],
"rules": {
// Disable general rules in test files
"@typescript-eslint/unbound-method": "off",
// Enable test-specific variants
"vitest/unbound-method": "error"
}
}
]
}

Test File Patterns

  • Naming conventions: *.test.ts, *.spec.ts, __tests__/**
  • Console allowance: Test files should allow console.log for debugging
  • Type strictness: Can relax for test doubles and mocks

Application to chatline

Current Issue

  • tsconfig.eslint.json is checking test files with exactOptionalPropertyTypes: true
  • Test builders create Partial<Message> with undefined values
  • Type incompatibility: undefined not assignable to string | null

Root Cause

Test helper builders intentionally create flexible object structures that don't match strict schema definitions. This is normal and acceptable.

Rationale:

  • Aligns with TypeScript official patterns (separate tsconfig files)
  • Tests are validated by Vitest runtime
  • Lint typecheck focuses on production code
  • Simpler to maintain

Implementation:

// tsconfig.eslint.json
{
"compilerOptions": { "noEmit": true },
"exclude": ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "tests/**"],
"include": ["src/**/*.ts", "scripts/**/*.ts"]
}

Rationale:

  • Source code src/** must typecheck cleanly
  • Scripts must typecheck cleanly
  • Tests have their own runtime validation via Vitest
  • Separate concerns: lint ≠ test validation

Option B: Relax Strictness Selectively (if including tests)

If tests must be linted for type safety:

// tsconfig.eslint.json - if tests must be included
{
"compilerOptions": {
"exactOptionalPropertyTypes": false, // <-- Relax for test flexibility
"noEmit": true,
"strict": true
},
"exclude": ["node_modules", "dist"]
}

Trade-off: Weakens type safety for test code (still better than no checking)


Comparison Table

AspectOption A (Exclude Tests)Option B (Relax Strictness)
Aligns with TypeScript docs✅ Yes❌ No
Preserves production strictness✅ Full strict checking on src/⚠️ Weaker overall
Maintenance burden✅ Low⚠️ Medium
Test validation✅ Vitest runtime⚠️ Limited lint checks
Follows antfu/ESLint✅ Use ESLint overrides✅ Separate strictness
Recommended forLibraries, production codeLegacy mixed strictness

Implementation Plan

Phase 1: Update tsconfig.eslint.json (EXCLUDE TESTS)

{
"compilerOptions": {
"noEmit": true,
"paths": {
"#enrich/*": ["./src/enrich/*"],
"#ingest/*": ["./src/ingest/*"],
"#normalize/*": ["./src/normalize/*"],
"#render/*": ["./src/render/*"],
"#schema/*": ["./src/schema/*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**",
"tests/**"
],
"extends": "./tsconfig.base.json",
"include": ["src/**/*.ts", "scripts/**/*.ts", "vitest.config.ts"]
}

Conclusion

Choose Option A (Exclude Tests from Lint Typecheck)

Reasoning:

  1. Official alignment: Matches TypeScript handbook patterns
  2. Community consensus: Both antfu (ESLint) and Vitest recommend file-scoped overrides, not strictness changes
  3. Clear separation: Production code = strict, tests = linted but flexible
  4. Maintainability: No need to track test-specific strictness exceptions
  5. Precedent: Major projects (Vue, Angular, Vite) all use this pattern

Exceptions to handle with ESLint overrides:

  • no-console: off in tests (for debugging)
  • @typescript-eslint/no-explicit-any: off in test helpers (for mock factories)
  • No need to change TypeScript strictness at all

References