Psalm Type Annotations
debt(d3/e3/b3/t5)
Closest to 'default linter catches the common case' (d3), since Psalm itself (listed in detection_hints.tools) flags missing/imprecise annotations when run, and it's a standard part of PHP static-analysis tooling.
Closest to 'simple parameterised fix' (e3), as quick_fix shows adding annotations like @psalm-return array{...} or @psalm-immutable is a docblock-level change per class/function, not a one-liner but a small pattern replacement.
Closest to 'localised tax' (b3), since annotations apply across web/cli/queue contexts but are docblock metadata — they don't reshape architecture, though maintaining annotation accuracy is an ongoing tax.
Closest to 'notable trap' (t5), grounded in the misconception that Psalm and PHPStan annotations are fully interchangeable — they overlap heavily but @psalm-* prefixes and generics support differ, a documented gotcha devs eventually learn.
Also Known As
TL;DR
Explanation
Psalm's annotation system extends PHP's type declarations with expressive docblock tags. @param and @return accept Psalm's extended type syntax: non-empty-string, positive-int, list<User>, array{id: int, name: string}. @template T enables generic class and function definitions: @template T @param array<T> $items @return T|null. @psalm-type creates reusable type aliases: @psalm-type UserId = positive-int. @psalm-suppress suppresses specific issues with a required code. @psalm-assert (and @psalm-assert-if-true) teach Psalm about custom assertion functions. @readonly (pre-8.1) marks properties immutable. Psalm's @var inline annotations narrow types within a method body when inference falls short. All annotations are backward-compatible — they live in docblocks and don't affect runtime.
Common Misconception
Why It Matters
Common Mistakes
- Not using @psalm-template for generic collection classes — Psalm cannot infer the element type without it.
- Using @return array when @return array<string, User> gives Psalm precise type information.
- Not running Psalm in CI — annotations are useless if violations are never surfaced.
- Suppressing Psalm errors with @psalm-suppress without understanding the underlying type issue.
Code Examples
// Without Psalm annotations — type information lost:
/** @return array */
public function findAll(): array { /* returns User[] */ }
$users = $repo->findAll();
$users[0]->email; // Psalm: cannot determine type of $users[0]
// With annotation:
/** @return array<int, User> */
public function findAll(): array { /* returns User[] */ }
$users[0]->email; // Psalm: knows this is User — validates property access
// Psalm type annotations — extend PHP's type system
/** @param non-empty-string \$email */
function sendEmail(string \$email): void {}
/** @return list<User> */ // ordered array, integer keys 0..n
function getUsers(): array {}
/** @template T */
function first(array \$items): mixed {}
/** @psalm-immutable */
class Money {
public function __construct(
public readonly int \$amount,
public readonly string \$currency,
) {}
}
// Taint analysis — track user input through the codebase:
/** @psalm-taint-source input */
function getUserInput(): string { return \$_POST['data'] ?? ''; }
/** @psalm-taint-sink sql */
function runQuery(string \$sql): void {}
// psalm will warn if tainted input flows to a SQL sink without sanitisation