Measuring and Improving Type Coverage: Hunting Down any
AI generated
<T>
type
TypeScript · Type Coverage · Code Quality · CI/CD
Measuring and Improving Type Coverage
How to hunt down any systematically instead of by chance

A project without compiler errors is not automatically type safe. any spreads unnoticed, through external libraries without types, through JSON.parse, and through implicitly inferred parameters. Type coverage makes these gaps visible, quantifies them in a single metric, and can be locked down as a CI gate against creeping regression.

13 min read type-coverage · CI gates · eliminating any TypeScript 5.x

1. What type coverage measures and why error-free isn't enough

Type coverage describes the share of all type positions in a codebase that actually resolve to a concrete type rather than to any. A project can have zero compiler errors and still show alarmingly low type coverage, because any itself is not an error, but a valid, if typeless, state. The compiler does not complain when a variable is any, because any is compatible with every other type by definition.

This exact property is what makes any so dangerous in larger projects: it spreads in both directions. A function that accepts an any parameter passes that loss of type information on to every place reusing its return value, often across several function calls, without a single warning appearing anywhere. Type coverage makes this creeping problem visible by producing a single, measurable percentage: what fraction of all expressions in the project are actually type checked, instead of hiding behind any.

For teams with grown codebases, especially after a JavaScript to TypeScript migration, type coverage is often the more honest metric than "zero compiler errors", because it reveals how much of the nominally migrated code actually benefits from real type checking and how much only changed its file extension.

2. The type-coverage tool: installation and a first report

The most common tool for measurement is the npm package type-coverage, which analyzes an existing TypeScript project and outputs a single percentage. After installation, a single command is enough to determine the current type coverage for the entire codebase as defined in tsconfig.json. The tool internally uses the same TypeScript Compiler API and the same TypeChecker as tsc itself, so the results stay consistent with what the regular build actually sees.

The first run on a grown project often produces a surprisingly low number, frequently well below 90 percent, even if the project compiles completely error free. This first report is the starting point for every further improvement and should be recorded as a baseline before anything in the code is changed.


# Install as a dev dependency
npm install --save-dev type-coverage

# Run against the project's tsconfig.json
npx type-coverage

# Example output
# 18234 / 19850 (91.85%)

# Fail the command (non-zero exit code) if coverage drops below a threshold
npx type-coverage --at-least 92

3. How type-coverage counts internally: any, implicit any and special cases

type-coverage checks, for every identifier position in the program, whether the TypeChecker resolves it to a concrete type or to any, for both explicitly written any and implicitly inferred any, which arises when the compiler cannot derive any other type. Both cases count against type coverage, because both have the same practical effect: nothing is checked at that spot.

An important nuance concerns unknown, the type safe counterpart to any. unknown counts as fully type checked, because the compiler refuses every operation on unknown until the type has been explicitly narrowed, while any permits every operation without complaint. This distinction is one of the strongest practical levers for improving type coverage: where migrating from any to a concrete type isn't immediately possible, unknown followed by a type guard is almost always the better intermediate solution.


// Counts against Type Coverage: explicit any
function parseConfigBad(raw: any) {
  return raw.database.host; // No check at all, fails silently at runtime
}

// Also counts against Type Coverage: implicit any (no annotation, no inference source)
function handleEventBad(event) {
  console.log(event.target.value); // event is implicitly any
}

// Does NOT count against Type Coverage: unknown forces an explicit check
function parseConfigGood(raw: unknown) {
  if (
    typeof raw === "object" &&
    raw !== null &&
    "database" in raw &&
    typeof (raw as { database: unknown }).database === "object"
  ) {
    return (raw as { database: { host: string } }).database.host;
  }
  throw new Error("Invalid config shape");
}

4. Wiring type coverage into CI gates with thresholds

The practical benefit of type coverage only unfolds once the measurement happens continuously in the CI pipeline instead of once. The --at-least parameter makes type-coverage exit with a non-zero code as soon as measured coverage drops below the given threshold, letting a pull request that worsens coverage get blocked automatically, just like a failing test.

The threshold should not be the theoretical goal of 100 percent, but the current baseline, measured with the first report from section 2. This lets the CI gate reliably prevent further regression without blocking the team with an unrealistic hurdle. The threshold is then raised step by step once real improvements have been measured, never the other way around.


# .github/workflows/type-coverage.yml
name: Type Coverage Gate
on: [pull_request]

jobs:
  type-coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      # Fails the build if coverage drops below the current baseline
      - run: npx type-coverage --at-least 91 --strict --ignore-catch

// package.json excerpt: keep the threshold and options in one place
{
  "scripts": {
    "type-coverage": "type-coverage --at-least 91 --detail",
    "type-coverage:ci": "type-coverage --at-least 91 --strict"
  },
  "typeCoverage": {
    "atLeast": 91,
    "strict": true,
    "ignoreCatch": true,
    "cache": true
  }
}

5. Typical sources of poor type coverage

In practice, poor type coverage usually concentrates on a few recurring sources rather than being randomly scattered any. External JavaScript libraries without shipped or @types available type definitions are a common source, because every use of such a library automatically returns any unless a custom declaration file is written. Just as common are DOM events, whose target property is only typed as EventTarget instead of a concrete input element without an explicit type assertion.

JSON.parse() is a third, particularly frequent source, because the function's type definition always returns any, regardless of what is actually parsed. Every unmodified use of JSON.parse() without a subsequent type check reduces type coverage by the number of property accesses on the result, often noticeably more than is visible at first glance.

6. Hunting down any deliberately: the detail mode

A single percentage alone helps little if it isn't clear where the remaining any spots actually are. The --detail parameter lists every single found any position with file, line and column, letting you deliberately prioritize which file to tackle first, for example the one with the most hits or the most business critical one.

For larger projects it's also worth aggregating by file or directory to spot systematic clusters, for example an entire legacy module that hasn't been migrated yet, instead of many small individual spots scattered across the whole codebase. This aggregation can be derived easily from the --detail output by counting and sorting file paths.


# List every any occurrence with file, line and column
npx type-coverage --detail

# Example output
# src/legacy/price-calculator.ts:34:12: any
# src/legacy/price-calculator.ts:41:8: any
# src/api/fetch-wrapper.ts:12:22: any
# ...

# Aggregate by file to find the worst offenders
npx type-coverage --detail | grep -oE '^[^:]+' | sort | uniq -c | sort -rn | head -10

7. Improving grown codebases step by step

Eliminating every any spot in one go in a large, grown codebase is rarely realistic and often blocks actual feature work for weeks. The more pragmatic route is a step by step strategy, similar to a gradual TypeScript migration: the CI threshold gets used as a ratchet, allowed to move only upward, never downward, so every new code change holds coverage steady at minimum, and usually improves it slightly.

For spots where any should deliberately and temporarily stay, // @ts-expect-error with a clear comment is preferable to silent any, because @ts-expect-error itself triggers a compiler error as soon as the line below actually becomes error free, preventing a piece of technical debt from lingering unnoticed after its root cause has long been fixed. New files should be written with a strict standard from the start, while existing files get pulled up gradually through the ratchet mechanism.

8. Fixing concrete any sources: JSON.parse and event handlers

The two main sources named in section 5 can be systematically fixed with manageable effort. For JSON.parse(), a generic wrapper with a runtime validator such as Zod is the most robust solution, because it not only sets the correct TypeScript type, but also actually checks at runtime whether the parsed data matches the expected structure, instead of just faking an unproven claim to the compiler via a type assertion.

For DOM event handlers, precisely typing the event parameter, for example event: Event & { target: HTMLInputElement } or the generic ChangeEvent<HTMLInputElement> from React style type definitions, fixes the problem at its root, without needing a type assertion at every single access point.


import { z } from "zod";

// Generic, runtime-validated JSON.parse wrapper — replaces "any" with real safety
function parseJsonSafe<T>(raw: string, schema: z.ZodType<T>): T {
  const parsed: unknown = JSON.parse(raw);
  return schema.parse(parsed); // Throws with a clear message if the shape doesn't match
}

const ConfigSchema = z.object({
  database: z.object({ host: z.string(), port: z.number() }),
});

const config = parseJsonSafe(rawConfigString, ConfigSchema);
console.log(config.database.host); // Fully typed, no any anywhere

// Typed event handler instead of an implicit any parameter
function handleInputChange(event: Event) {
  const target = event.target as HTMLInputElement;
  console.log(target.value); // No any, precise type at the boundary
}

9. Type coverage compared to strict mode and an ESLint rule

Several tools target related but not identical problems. The following overview places type coverage against strict compiler mode and an ESLint rule for any.

Tool Measures Result Typical use
type-coverage Percentage type coverage, including implicit any A single metric, CI-gate capable Project wide progress and preventing regression
strict: true in tsconfig Compiler errors for missing type safety Binary: build passes or fails Enforcing a baseline safety level
ESLint no-explicit-any Only explicitly written any in source Per-line warning, editor integration Making new any spots visible immediately in review
Custom Compiler API check Any project specific type rule Freely definable Very specific architecture rules

Type coverage and strict: true complement rather than replace each other: strict prevents new code from containing obvious type errors, while type coverage quantifies the remaining, often larger gray area of explicit and implicit any that strict alone does not eliminate. The ESLint rule no-explicit-any complements both, because it's visible immediately in the editor, but doesn't catch implicit any, which is why none of the three tools fully replaces the others.

Mironsoft

TypeScript code quality, CI gates and Magento/Hyvä integrations

How high is your project's actual type coverage?

We analyze existing TypeScript projects for hidden any spots, set up type-coverage as a CI gate, and accompany the step by step increase of type coverage without blocking ongoing feature development.

Type coverage audit

Measure baseline, identify and prioritize any clusters

CI gate setup

Setting up ratchet thresholds in GitHub Actions or GitLab CI

any elimination

Runtime-validated replacements for JSON.parse and event handling

10. Summary

Type coverage closes an important gap that plain error freedom leaves open: it measures what percentage of the actual code benefits from real type checking instead of hiding behind explicit or implicit any. The type-coverage tool uses the same TypeChecker as tsc itself, delivers a single, CI-ready percentage, and can be used with --at-least as a ratchet against regression.

The most common sources of poor type coverage, external libraries without types, JSON.parse(), and untyped DOM events, can be fixed with targeted, repeatable patterns: runtime validation instead of blind type assertions, unknown instead of any as a safe intermediate step, and @ts-expect-error instead of silent any for deliberate, temporary exceptions. Combined with strict compiler mode and an ESLint rule, this creates a layered safety net that none of the three tools could provide alone.

Measuring and Improving Type Coverage - The Essentials at a Glance

Measuring

npx type-coverage returns a percentage via the TypeChecker, counting both explicit and implicit any.

CI gate

Use --at-least as a ratchet, measure a baseline, only move the threshold upward.

Main sources

Untyped libraries, JSON.parse without validation, DOM events without precise typing.

Fix strategy

unknown instead of any as an intermediate step, runtime validation with Zod, @ts-expect-error for deliberate exceptions.

11. FAQ: Measuring and Improving Type Coverage

1What is type coverage?
The share of type positions resolving to a concrete type instead of any.
2Isn't error freedom enough?
any is not an error. An error-free project can still contain large unchecked areas.
3Installing type-coverage?
npm install --save-dev type-coverage, then npx type-coverage or with an --at-least threshold.
4unknown as bad as any?
No, unknown counts fully, since every operation requires explicit checking first.
5Setting up a CI gate?
--at-least THRESHOLD in the pipeline, measure a baseline, only move it upward.
6Typical any sources?
Untyped libraries, JSON.parse without checking, DOM events with an untyped target.
7Finding concrete any spots?
--detail lists every position with file, line and column, aggregatable for prioritization.
8JSON.parse without faking safety?
A generic wrapper with a runtime validator like Zod checks type and actual structure together.
9@ts-expect-error instead of any?
Triggers an error itself once the line becomes error free, preventing unnoticed leftover debt.
10Does it replace strict mode or ESLint?
No, all three complement each other and cover different parts of the same problem.