Psalm Type Syntax
Every type expression Psalm 7 accepts inside a docblock, for writing stubs and for building Union/Atomic values in handlers. Verified against Psalm 7.0.0-beta24.
Companion pages: Docblock Annotations (the tags that carry these types), Purity and Capabilities (the [pure] part of callable and iterable types), Taint Analysis Stubs.
Ground truth when this page and Psalm disagree:
vendor/vimeo/psalm/src/Psalm/Internal/Type/TypeTokenizer.php:PSALM_RESERVED_WORDS, the words never resolved as class namesvendor/vimeo/psalm/src/Psalm/Type/Atomic.php:createInner(), keyword toAtomicmapvendor/vimeo/psalm/src/Psalm/Internal/Type/TypeParser.phpandParseTreeCreator.php: generics, shapes, callables, conditionals- Upstream prose: type syntax docs
To check what Psalm makes of an expression, put /** @psalm-trace $x */ on a statement and run vendor/bin/psalm --no-cache --threads=1 on a scratch file. The Atomic column names the class a handler gets back or constructs.
Scalars, top and bottom
| Type | Atomic | Notes |
|---|---|---|
int, float, string, bool | TInt, TFloat, TString, TBool | integer, double, real, boolean are legacy aliases |
true, false, null | TTrue, TFalse, TNull | |
void | TVoid | Return position only |
scalar | TScalar | int\|float\|string\|bool |
numeric | TNumeric | int\|float\|numeric-string |
array-key | TArrayKey | int\|string |
mixed | TMixed | Top type |
never | TNever | Bottom type. Aliases: no-return, never-return, never-returns, empty |
object | TObject | Any object |
resource | TResource | |
closed-resource | TClosedResource | |
iterable, iterable<V>, iterable<K, V> | TIterable | array\|Traversable. Takes a purity: iterable[pure]<K, V> (purity) |
Integer subtypes
| Type | Atomic | Notes |
|---|---|---|
positive-int | TIntRange | int<1, max> |
non-negative-int | TIntRange | int<0, max> |
negative-int | TIntRange | int<min, -1> |
non-positive-int | TIntRange | int<min, 0> |
int<5, 10>, int<min, 0>, int<1, max> | TIntRange | Bounds are int literals or min/max. int<min, max> is plain int |
literal-int | TNonspecificLiteralInt | Any int written as a literal in source |
int-mask<1, 2, 4> | TIntMask | Any bitwise OR of the listed values (literals or single class constants) |
int-mask-of<Foo::FLAG_*> | TIntMaskOf | Same, from a wildcard constant set; also accepts key-of<...> / value-of<...> |
String subtypes
| Type | Atomic | Notes |
|---|---|---|
non-empty-string | TNonEmptyString | |
non-falsy-string | TNonFalsyString | Not '' and not '0'. Alias: truthy-string |
numeric-string | TNumericString | Passes is_numeric() |
lowercase-string, non-empty-lowercase-string | TLowercaseString, TNonEmptyLowercaseString | |
literal-string, non-empty-literal-string | TNonspecificLiteralString, TNonEmptyNonspecificLiteralString | Built only from literals in source |
callable-string | TCallableString | Passes is_callable() |
class-string, class-string<Foo> | TClassString | FQCN of Foo or a subtype; Foo may be a template |
interface-string, interface-string<Foo> | TClassString | Interface names only |
enum-string, enum-string<Foo> | TClassString | Enum names only |
trait-string | TTraitString | |
Foo::class | TLiteralClassString |
Intersections of string refinements collapse: non-empty-string&lowercase-string is non-empty-lowercase-string.
Literals and constants
| Type | Atomic | Notes |
|---|---|---|
42, -3, 1_000 | TLiteralInt | _ separators allowed for ints only |
3.14, -0.5 | TLiteralFloat | No _ separators |
'foo', "foo" | TLiteralString | No heredoc/nowdoc form |
Foo::BAR | TClassConstant | Resolved to the constant’s literal type |
Foo::BAR_*, Foo::* | TClassConstant | Union of the matching constants. Trailing * or bare * only |
Suit::Hearts | TEnumCase | Same syntax as a constant; becomes an enum case when Suit is an enum |
Arrays and lists
| Type | Atomic | Notes |
|---|---|---|
array | TArray | array<array-key, mixed> |
array<V>, array<K, V> | TArray | One parameter means array<array-key, V>. V[] also works |
non-empty-array<K, V> | TNonEmptyArray | |
list<V>, non-empty-list<V> | TKeyedArray | Keys 0..n-1 in order |
associative-array<K, V> | TArray | Plain alias of array |
callable-array, callable-list | TKeyedArray | [class-string\|object, non-empty-string] pair |
Shapes
array{id: int, name?: string} // sealed; `?` = key may be absent
array{0: string, 1: int} // explicit int keys
array{string, int} // implicit keys 0, 1 (cannot mix with explicit)
list{string, int} // list shape
array{id: int, ...} // unsealed: more keys of array-key => mixed
array{id: int, ...<string, int>} // unsealed with typed extras
list{string, ...<int>} // unsealed list
array{} // the empty array
callable-array{Foo, 'bar'} // shape that must be callable
object{id: int, name?: string} // object shape (TObjectWithProperties)
Keys may be quoted ('quoted key': T) or Foo::class; other Foo::CONST keys are rejected. Object shapes have no unsealed form: object{id: int, ...} parses and drops the ....
Objects
| Type | Atomic | Notes |
|---|---|---|
Foo | TNamedObject | |
Foo<A, B> | TGenericObject | Class must declare matching @templates |
Foo[pure]<A> | TGenericObject | Purity argument for a class with @psalm-purity-template (purity) |
self, static, parent | TNamedObject | self and parent resolve to the FQCN; static stays late-bound |
$this | TNamedObject | Same as static |
Generator<K, V, TSend, TReturn> | TGenericObject | Fewer parameters: missing ones become mixed; Generator<V> sets the value |
Traversable<K, V>, Iterator<K, V>, IteratorAggregate<K, V> | TGenericObject | One parameter is the value type. All take a purity: Iterator[pure]<K, V> |
callable-object | TCallableObject | Has __invoke() |
stringable-object | TObjectWithProperties | Has __toString() |
arraylike-object<K, V> | intersection | Traversable<K, V>&ArrayAccess<K, V>&Countable |
Callables
callable // TCallable
Closure // TClosure
callable(int, string): bool // typed
Closure(int, string=): void // `=` optional parameter
callable(int, string...): void // `...` variadic parameter
callable(int $id, string ...$rest): void // named parameters
pure-callable(int): int // same as callable[pure](int): int
Closure[read-props|io](): void // explicit capability set
Closure[P](): void // purity template
Closure[pure] // any pure closure, no parameter list
By-reference parameters are not expressible: callable(int &$x): void is an InvalidDocblock. A named parameter cannot carry = (string $name=) or a ... before its name (string... $rest): Psalm 7.0.0-beta24 crashes the whole scan on both; write the name only on required or ...$rest parameters. The bracket part is a capability set; see Purity and Capabilities for the names, templates, and the Closure[_] parameter form.
Utility types
| Type | Atomic | Notes |
|---|---|---|
key-of<T> | TKeyOf, TTemplateKeyOf | Keys of an array, shape, constant array, or template |
value-of<T> | TValueOf, TTemplateValueOf | Values; on a backed enum, the backing values (value-of<Suit> is 'h'\|'s') |
properties-of<T> | TPropertiesOf, TTemplatePropertiesOf | Properties as a keyed array |
public-properties-of<T>, protected-properties-of<T>, private-properties-of<T> | TPropertiesOf | Visibility filtered |
class-string-map<T of Foo, T> | TClassStringMap | Maps each class-string key to an instance of it. as works in place of of |
T[K] | TTemplateIndexedAccess | T and K must both be templates of the same declaration |
Conditional types
(Subject is Type ? IfTrue : IfFalse), where Subject is a template, a $param, or func_num_args(). Parameter subjects work in @return, @psalm-self-out and @psalm-this-out.
/** @return ($key is null ? array<string, mixed> : mixed) */
/** @return (T is string ? int : float) */
/** @return (func_num_args() is 0 ? static : string) */
/** @return ($num is int ? int : ($num is float ? float : int|float)) */
/** @return (TArray is array<never, never> ? null : TValue) */
Plugin traps: write static, never $this, on the chainable branch of a stub conditional (#888, hard rule 3 in AGENTS.md); func_num_args() is unsound under a leading spread argument.
Union, intersection, nullable
int|string // union
?string // string|null
Foo&Bar // intersection: object types only (or keyed arrays)
Templates and aliases
A declared @template T is a TTemplateParam wherever it appears as a type. A class template may carry a default (@template T of Bound = Default): a generic type with fewer arguments than templates is read with the defaults filled in, a bare class name reads the bounds instead, and a type built by a plugin handler is never filled (see template defaults). Type aliases are declared with @psalm-type Name = ... and pulled in with @psalm-import-type Name from Foo [as Local] (TTypeAlias). Declaration tags live on the annotations page.
Accepted but misleading
Syntax that parses without the meaning its name suggests. Do not use it in stubs.
| Written | What Psalm does |
|---|---|
uppercase-string, non-empty-uppercase-string | Plain string / non-empty-string (unimplemented in Psalm) |
open-resource | Reserved word but no type: InvalidDocblock |
non-empty-countable | Reserved word Psalm uses internally (from count() checks); not writable, InvalidDocblock even in @psalm-assert |
empty | never, not “empty value” |
self-accessing-callable, self-mutating-callable (and -Closure) | Removed in 7.0.0-beta23; now parse as impure callable. Use callable[read-props] / callable[read-props\|write-this-props\|write-refs] |
impure-callable, impure-Closure | Same as bare callable / Closure |
object{a: int, ...} | Unsealed marker silently dropped |
callable(int &$x): void | InvalidDocblock, the whole type is lost |
callable(string $name=): void, callable(string... $rest): void | Uncaught exception, Psalm aborts the run |