skip to content

What does Pest's --type-coverage option measure, and how does it differ from running Pest with --coverage?

level: middleimportance: nice to knowfreq 18%

answer

  1. declarations, not executed lines
  2. separate plugin: pest-plugin-type-coverage
  3. no tests or coverage driver needed
  4. rt31 and pa31 in the report
  5. --type-coverage --min=100

basics

~10 s

--type-coverage, from pestphp/pest-plugin-type-coverage, reports the percentage of parameters, return types and properties that carry type declarations, analysing the code without running tests; --coverage runs the tests and reports which lines executed.

solid answer

~40 s

Type coverage is a separate plugin, `pestphp/pest-plugin-type-coverage`. `./vendor/bin/pest --type-coverage` analyses the source and prints, per file, the share of declarations that have a type, with markers such as `rt31` (missing return type on line 31) and `pa31` (missing parameter type on line 31). It runs no tests and needs no coverage driver. `--coverage`, by contrast, runs the suite under Xdebug or PCOV and reports which lines executed. The two gate differently: `--type-coverage --min=100` fails when any declaration is untyped, `--coverage --min=80` when too few lines ran. Type coverage counts whether a type is declared, not whether it is correct; checking that is a static analyser's job. `@pest-ignore-type` exempts a line and `--compact` hides fully typed files.

code

bash · 4 lines
bash
composer require pestphp/pest-plugin-type-coverage --dev

# only files below 100%, and fail the build if anything is untyped
./vendor/bin/pest --type-coverage --compact --min=100

go deeper

for a junior

Recall that --type-coverage counts typed parameters, returns and properties, and that --min makes it fail below a threshold.

for a middle

Contrast static type coverage with dynamic code coverage: no tests, no coverage driver, a separate plugin, and markers like rt31 and pa31.

for a senior

Use it as a ratcheting gate on a legacy codebase and explain why it complements, but never replaces, a static analyser.

for a principal

Decide which quality signals a team gates on (type coverage, static analysis level, code coverage, mutation score) and in what order they are introduced.

## What type coverage measures **Type coverage** is the percentage of places in your code that could carry a type declaration and actually do: function and method parameters, return types, and properties. In Pest it comes from a separate plugin: ```bash composer require pestphp/pest-plugin-type-coverage --dev ./vendor/bin/pest --type-coverage ``` The report lists each file with its percentage and, for files below 100 percent, a marker per missing declaration. The docs define two of them: `rt31` means the **return type** of the function on line 31 is missing, and `pa31` means a **parameter type** on line 31 is missing. A total percentage closes the report. ## How it differs from code coverage | | `--type-coverage` | `--coverage` | |---|---|---| | Question answered | how much of the code declares types | how much of the code the tests executed | | Runs the tests | no | yes | | Needs Xdebug or PCOV | no | yes | | Plugin | `pestphp/pest-plugin-type-coverage` | built into Pest, on PHPUnit's coverage | | Gate | `--type-coverage --min=100` | `--coverage --min=80` | - Type coverage is a **static** check. It looks at declarations in the source, so it is fast and works on code that has no tests at all. - Code coverage is **dynamic**. It depends on which tests exist and what they exercise, and it says nothing about types. - The two are independent: a fully typed class can have zero tests, and a heavily tested class can be untyped. ## What it does not tell you 1. It does not check that a declared type is **correct** or consistent with how the value is used. That is the job of a static analyser such as PHPStan. 2. It does not say whether a type is **precise**: the useful reading is "was a type written", not "is it the narrowest type that fits". 3. It is not a substitute for `declare(strict_types=1)`: a declared `int` parameter can still receive a coercible string from a caller that runs in coercive mode. ## Using it as a gate - Enforce it in CI with `./vendor/bin/pest --type-coverage --min=100` on new projects, or with the current percentage on an older codebase, raised over time. - `--compact` limits the report to files below 100 percent, which is what you read during a cleanup. - `--type-coverage-json=report.json` writes the result to a file for tooling. - A deliberate gap, for example a framework property whose parent class declares no type, is exempted with a trailing `// @pest-ignore-type` comment on that line. ## Reading a report on a legacy codebase On a codebase written before scalar type declarations were common, the first report is long. A workable reading order: - Start with `--compact` so only files below 100 percent appear. - Fix **return types** first. They are cheap to add, and a declared return type immediately tells callers and static analysers what comes back. - Then fix **parameter types** on public methods, where a wrong value from a caller does the most damage. - Leave properties inherited from framework base classes for last; they are the usual candidates for `@pest-ignore-type`. - Record the total after each pass and raise `--min` to it, so the number can only go up. ## Where it fits in a toolchain A typical PHP 8.5 pipeline pairs three independent signals: - **Type coverage** to make sure declarations are written at all. - **Static analysis** to check the declared and inferred types against each other. - **Tests with code coverage** to check behaviour. Type coverage is the cheapest of the three and the quickest to explain to a team adopting typed PHP, which is why it is often the first gate introduced on a legacy codebase that is being typed file by file.

  • The type coverage report shows rt31 next to a file; what do you change?
    Add a return type declaration to the function or method declared on line 31, for example `: array` or `: void`. `pa` markers would instead point to a parameter without a type. After the fix the file should reach 100 percent in the next run.
  • Why can a project have 100 percent type coverage and still fail PHPStan?
    Type coverage only counts whether a declaration exists. It does not check that the declared types agree with the values passed and returned, which is what a static analyser does.

saying these in an interview costs you the question

  • --type-coverage needs Xdebug or PCOV to be installed
  • Type coverage only counts lines the tests executed
  • 100 percent type coverage means the types are all correct
  • Type coverage is built into Pest without installing a plugin