---
title: "Five Ways to Write a PHP Type"
url: https://www.exakat.io/five-ways-to-write-a-php-type/
date: 2026-06-20
modified: 2026-06-21
author: "dams"
description: "Five Ways to Write a PHP Type PHP's type system is grown up. PHP 7.0 started the ball rolling with parameter and return types. PHP 7.4 added property types. PHP..."
categories:
  - "Technology"
tags:
  - "argument"
  - "instanceof"
  - "property"
  - "returntype"
  - "type"
image: https://www.exakat.io/wp-content/uploads/2026/06/batteur.320.jpg
word_count: 1925
---

# Five Ways to Write a PHP Type

# Five Ways to Write a PHP Type

PHP's type system is grown up. PHP 7.0 started the ball rolling with parameter and return types. PHP 7.4 added property types. PHP 8.3 added the class constant types, though we'll omit this for now. PHP has quietly accumulated five distinct syntaxes where you can write something that looks like a type: parameter types, property types, return types, `catch` clauses, and the `instanceof` operator. Five positions, one similar looking syntax, and, wait for the end, five different rulebooks.

If you have ever pasted a type from a catch clause into a property declaration and watched PHP throw a compile error in agony, you've already bumped into one of those rulebooks. This post covers all five of them.

## A quick vocabulary note

PHP's type system is made of several distinct categories:

- **Named types**: a class or interface name: `MyException`, `Countable`, `DateTimeInterface`.
- **Scalar types**: `int`, `float`, `string`, `bool`.
- **Pseudo-types**: `mixed`, `callable`, `iterable`, `object`: categories of value, not class names.
- **Literal types**: `true` (PHP 8.2) and `false` (PHP 8.0): `bool`, narrowed down to a single value.
- **Relative types**: `self`, `parent`, `static`: resolved against the class where the code is written.
- **Return-only types**: `void` and `never`: they describe what a function does at exit, not what a value holds.
- **Composite types**: union (`A|B`), intersection (`A&B`), and DNF (`(A&B)|C`).

Each position has its own opinion about which of these it will tolerate. None of them accepts the full list.

## Parameter types: the generous generalist

Parameters accept the widest range. Scalars, named classes, pseudo-types, nullables, literals, relative types, unions, intersections, DNF. The only notable absences are `void`, `never`, and `static`.

`void` and `never` make no sense here: you cannot pass "no value": what about not passing it AT ALL? And there is no such thing as a value of type `never`. `static` is slightly more philosophical. It was deliberately excluded from parameters when it was added as a return type in PHP 8.0. The reason involves variance, which is its own article, best read with coffee.

Parameters are contravariant in inheritance: a child class may widen a parameter type. This is the one type position where being less precise is actually the correct move.

## Property types: parameters, minus one

Property types accept almost everything parameters do, with one exception: `callable` is forbidden.

The reason is mildly amusing. PHP can verify that a value is callable at the moment of assignment: an array `['MyClass', 'method']` satisfies the check, but callability is not a durable property. If `MyClass` is later unloaded, the property is now holding a non-callable value that passed its type check perfectly. PHP decided not to carry that verification burden forward on every property read. The result is that `callable` works on parameters and return types but silently fails on properties.

This is the kind of asymmetry that looks like a bug until you understand it, and then it looks like a reasonable trade-off, and then you still find it annoying.

Property types are also invariant in inheritance: unlike parameters and return types, a child class cannot change a property's type. Not wider, not narrower. Exactly the same. Full stop.

## Return types: where void and never earn their keep

Return types accept everything parameters accept, plus `static`, `void`, and `never`.

`void` marks a function that does not return a value. You may still write `return;` inside it, which is a detail that confuses people for about ten minutes and then becomes natural. `never` is the more dramatic sibling: it marks a function that never returns at all: it throws, calls `die()`, or loops forever. It is the bottom type; no value is of type `never`, and nothing needs to be checked.

`static` as a return type means "an instance of whatever class was actually called at runtime." It is not the same as `self` which is resolved at compile time, `static` follows late static binding. The distinction matters for factory methods and fluent interfaces in inheritance hierarchies, and it is [explained thoroughly in the PHP manual](https://www.php.net/manual/en/language.oop5.late-static-bindings.php) if you want the full version.

Return types are covariant: each generation of a class may return something more specific than its parent declared. The arrow of precision points downward.

## catch clauses: union types, but stop right there

`catch` is where the first serious divergence appears. The clause accepts types, but it is not a type declaration. It is a selector: when an exception propagates up the call stack, PHP walks through the catch clauses in order and takes the first one whose type the thrown object satisfies. The type exists to be matched against a live object, and that shapes what it will accept.

Named classes are welcome, with a constraint: the class must implement `Throwable`. Trying to catch an arbitrary class that has nothing to do with exceptions is a compile-time error, not a runtime surprise. PHP checks this before your code runs.

Scalars, pseudo-types, null, literals, relative types, `void`, `never`: all are rejected. There is nothing to match against. You cannot throw a scalar, so you cannot catch one. `catch (mixed $e)` sounds almost reasonable until you remember that it would mean "catch everything," and that is what `catch` with a `\Throwable` type already does.

Union types, however, are accepted.

And here is the historical footnote worth pausing on: this syntax has been [valid since PHP 7.1](https://wiki.php.net/rfc/multiple-catch), which is three major versions before union types arrived everywhere else in PHP 8.0. `catch` was PHP's first experiment with multi-type syntax, quietly living in exception handling while the rest of the language had not caught up yet. It was years ahead of its time and then, as these things go, stopped where it was.

Because intersection types and DNF do not work in `catch`. You cannot write `catch ((Loggable&Throwable) $e)`. The runtime matching logic handles one class name at a time; the engine has simply never been extended to handle conjunctions. In practice this is rarely a hardship: a shared interface on the exception hierarchy covers most cases. But the gap is real.

One genuine quality-of-life improvement came with PHP 8.0: the variable is now optional. `catch (RuntimeException)` without the `$e` is valid when you have nothing useful to do with the exception object. The [RFC](https://wiki.php.net/rfc/anonymous_catch) for this feature is three sentences long, which feels right.

## instanceof: the most opinionated of all

`instanceof` is not a declaration. It is a boolean expression that asks, in real time, whether a value belongs to a class or interface. It never throws. If the left-hand operand is not an object, it returns `false` quietly rather than raising an error. If the named class does not exist, it returns `false` rather than halting.

What it accepts on the right-hand side is more restrictive than any other syntax.

Named classes and interfaces: yes. Scalars: no `$x instanceof int` is a parse error, and it makes sense that such syntax does not make sense (right?). Pseudo-types: also no, and this is where people stumble. `$x instanceof object` is rejected, even though testing whether something is an object is a perfectly ordinary need. For that, the answer is [`is_object()`](https://www.php.net/manual/en/function.is-object.php). Similarly, `$x instanceof mixed`, `$x instanceof iterable`, and `$x instanceof callable` are all parse errors.

Composite types: none. No union, no intersection, no DNF. There is no `$x instanceof A|B`. You write `$x instanceof A || $x instanceof B` instead, which is arguably clearer, and you think for a moment about whether that is enough.

But `instanceof` has two things no other type position has.

First, it accepts relative types, and uniquely, all three of them, including `static`.

This is the only place in PHP's type system where the type itself is determined at runtime, not at parse time. No other syntax supports a dynamic class name. It is the feature that makes `instanceof` genuinely useful for plugin systems, dispatchers, and deserialization layers, and it is the feature that makes static analysers quietly recalibrate their expectations.

## The full picture

| | Parameter | Property | Return | `catch` | `instanceof` |
| --- | --------- | -------- | ------ | ------- | ------------ |
| Named class/interface | ✓ | ✓ | ✓ | ✓ Throwable only | ✓ |
| Scalar | ✓ | ✓ | ✓ | ✗ | ✗ |
| `null` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `array` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `mixed` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `object` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `iterable` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `callable` | ✓ | ✗ | ✓ | ✗ | ✗ |
| `void` | ✗ | ✗ | ✓ | ✗ | ✗ |
| `never` | ✗ | ✗ | ✓ | ✗ | ✗ |
| `true` / `false` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `self` / `parent` | ✓ | ✓ | ✓ | ✗ | ✓ |
| `static` | ✗ | ✗ | ✓ | ✗ | ✓ |
| Union `A\|B` | ✓ | ✓ | ✓ | ✓ | ✗ |
| Intersection `A&B` | ✓ | ✓ | ✓ | ✗ | ✗ |
| DNF `(A&B)\|C` | ✓ | ✓ | ✓ | ✗ | ✗ |
| Nullable `?T` | ✓ | ✓ | ✓ | ✗ | ✗ |
| Dynamic variable | ✗ | ✗ | ✗ | ✗ | ✓ |
| Variance | contra | invariant | co | | |

## The underlying pattern

Step back and a structure emerges. The five positions sit on a spectrum from contract to probe.

Parameters, properties, and return types are contracts. They define what a function or class commits to accepting or producing, they are checked at call time, assignment time, or return time, and they participate in the inheritance system with variance rules. They need to be as expressive as possible, so they accept nearly the full vocabulary.

`catch` is a selector. It runs at exception time, it matches a live thrown object against a list of types, and it needs to identify concrete class membership. That rules out pseudo-types, scalars, and composites beyond union. The fact that it predates the general union type system is a piece of PHP history that most people have forgotten.

`instanceof` is a probe. It asks a yes-or-no question about a single value, it never fails loudly, and its only business is matching class identity. It is the most restrictive of the five positions in what it accepts as a type. Nut the most flexible in how that type can be expressed, including the unique ability to take it from a variable at runtime.

The syntax looks identical in all five places. The semantics are not. Copy a type from a `catch` clause to a property and PHP will accept it quietly, and add a simple `?` or `|null` to the catch clause type and the engine will refuse it at compile time. Try `(A&B)` in a catch clause or `callable` on a property and PHP will tell you so before your first test ever runs.

Once you have the map, the error messages stop feeling arbitrary. Each position is its own small dialect of the same language, with its own vocabulary and its own limits and they have good reasons for every rule they keep.

*Related reading:* [PHP type declarations manual](https://www.php.net/manual/en/language.types.declarations.php) · [Exceptions](https://www.php.net/manual/en/language.exceptions.php) · [instanceof](https://www.php.net/manual/en/language.operators.type.php) · [DNF types RFC](https://wiki.php.net/rfc/dnf_types) · [Intersection types RFC](https://wiki.php.net/rfc/pure-intersection-types) · [Anonymous catch RFC](https://wiki.php.net/rfc/anonymous_catch)