Combining union and intersection types
Plain union types describe an either or, plain intersection types describe a both at once, but some signatures need both at the same time. Disjunctive normal form types have allowed exactly that combination since PHP 8.2, with a clear but strictly enforced parenthesis syntax.
Table of Contents
- 1. Where plain union and intersection types hit a wall
- 2. DNF type syntax: mandatory parentheses and parser rules
- 3. Practical example: nullable intersection types
- 4. Practical example: event listeners with multiple capabilities
- 5. What DNF types do not allow
- 6. DNF types in properties and return types
- 7. DNF types in static analysis
- 8. Practical case: a decorator with multiple capability interfaces
- 9. Alternatives before PHP 8.2 and why DNF is the better solution
- 10. Summary
- 11. FAQ
1. Where plain union and intersection types hit a wall
Union types and intersection types are their own topics with their own basic rules, this is not about those basics, it is about the case where both are needed at once. A parameter might either be a simple type, or it might need to satisfy two interfaces at the same time: that combination cannot be expressed with plain union or plain intersection alone.
Before PHP 8.2, the only way out was an artificial marker interface that extends both original interfaces, or giving up on typing in favor of a documented but unchecked docblock. Both options cost either extra class hierarchy or actual type safety.
The term disjunctive normal form comes from propositional logic, where a formula is expressed as an or combination of several and groups. PHP 8.2 carries that exact pattern into the type system: every and group is an intersection, every or combination is a union, and together they form a DNF type with a fixed, predictable structure.
2. DNF type syntax: mandatory parentheses and parser rules
The syntax follows the pattern (A&B)|C: an intersection of multiple types is wrapped in parentheses and joined with further types through union. Those parentheses are not optional, they are required for every intersection inside a union type as soon as more than one type appears on the union side.
The parser allows any number of such parenthesized intersection groups within one union type, but no nesting of parentheses inside each other. An intersection cannot itself contain a parenthesized union, disjunctive normal form deliberately describes only one flat level of and groups joined by or.
function process((Countable&Iterator)|null $items): void
{
if ($items === null) {
return;
}
foreach ($items as $item) {
// $items is guaranteed to be both Countable and Iterator here
}
}
3. Practical example: nullable intersection types
A common use case combines an intersection of several interfaces with null as a third, alternative type, for example when an optional parameter can either be an object satisfying several capabilities at once, or no value at all. The previous example with (Countable&Iterator)|null shows exactly that.
Without DNF types, you would either have to give up typing the null case, or introduce an extra interface CountableIterator that artificially bundles both capabilities, just to model a single parameter signature. DNF types make that interface unnecessary.
4. Practical example: event listeners with multiple capabilities
An event system that accepts both simple listeners and listeners with prioritization logic benefits especially clearly from DNF types. A parameter can then be either a plain ListenerInterface, or an object that implements both PrioritizedInterface and ListenerInterface at once, without a third class hierarchy being necessary.
This kind of signature shows up often in middleware stacks and plugin systems, whenever optional extra capabilities such as prioritization, logging, or configurability need to be combinable independently, without each combination existing as its own named interface.
interface ListenerInterface
{
public function handle(object $event): void;
}
interface PrioritizedInterface
{
public function getPriority(): int;
}
function registerListener(ListenerInterface|(ListenerInterface&PrioritizedInterface) $listener): void
{
// Both a plain listener and a prioritized listener are accepted here
}
5. What DNF types do not allow
Nested parentheses are the most common stumbling block: an expression like ((A&B)|C)&D is rejected by the parser, because DNF types only allow a flat structure of parenthesized and groups joined by or, not the reverse, an or group nested inside an and combination.
Also not allowed is an intersection of two concrete, unrelated classes without a shared interface, since a class in PHP can only extend a single other class. Intersection types work reliably only with interfaces, or a combination of one class with several interfaces.
// Parse error: nested parentheses are not allowed in DNF types
function invalid(((Countable&Iterator)|ArrayAccess)&Stringable $value): void
{
}
6. DNF types in properties and return types
DNF types are not limited to parameters, they work identically in property declarations and return types. A method can return (JsonSerializable&Countable)|string, if depending on internal state it either returns a serializable, countable object, or an already rendered string.
Properties follow the same parenthesis requirement as parameters, and combining a DNF type with readonly is allowed as well: a readonly property can carry a DNF type without the immutability rules and the type syntax rules restricting each other.
7. DNF types in static analysis
PHPStan and Psalm correctly interpret DNF types as a combination of and and or conditions, and give precise autocompletion in IDEs for each combination, so that inside an if block narrowed to one union branch, every method of the relevant intersection becomes visible automatically.
For code quality that is a real gain over the old marker interface approach: analysis tools no longer need to understand an extra, artificial class hierarchy, they read the actually required type combination straight from the signature. Editors like PhpStorm benefit directly from this too, with no special plugin or dedicated handling for DNF types required.
8. Practical case: a decorator with multiple capability interfaces
In a decorator pattern that combines several independent extra capabilities such as caching, logging, and retry logic, DNF types let a factory function state exactly which combinations of capabilities are accepted, without maintaining a separately named interface for every combination.
That noticeably reduces the number of interfaces in a project, because not every conceivable combination of capabilities needs to be modeled upfront as its own type. Instead, combinations only appear where a signature actually needs them.
9. Alternatives before PHP 8.2 and why DNF is the better solution
Before PHP 8.2, there were essentially two alternatives: an extra, artificial marker interface that hardwires the needed combination, or giving up strict typing in favor of a docblock comment that no tool enforces at runtime.
Both alternatives come with a real cost: the marker interface bloats the class hierarchy and has to be extended for every new combination, the docblock approach loses the type system's guarantee entirely. DNF types solve both problems by expressing the combination directly and ad hoc in the signature, without an extra class and without giving up real type checking. For new projects, it is worth deliberately reviewing every spot where a marker interface exists purely for typing reasons.
| Feature | Union Type | Intersection Type | DNF Type |
|---|---|---|---|
| Meaning | Either or between types | Both at once | A combination of both |
| Parentheses required | Not needed | Not needed | Required around every intersection group |
| Since PHP version | PHP 8.0 | PHP 8.1 | PHP 8.2 |
| Nesting | Not relevant | Not relevant | Only one flat level allowed |
| Typical use | A parameter accepts several simple types | A parameter must satisfy several interfaces | A parameter needs an interface combination plus alternative |
| Example signature | int|string $id |
Countable&Iterator $items |
(Countable&Iterator)|null $items |
Mironsoft
PHP modernization, code quality, and legacy refactoring
Grown PHP code nobody wants to touch anymore?
We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.
Legacy Refactoring
Modernize grown PHP code in a structured, low-risk way.
Establishing Code Quality
Anchor PHPStan, coding standards, and CI checks sustainably in the team.
Version Upgrades
Plan and execute PHP major version upgrades safely, without downtime.
10. Summary
DNF Types
Core idea
DNF types combine union and intersection in one type, with mandatory parentheses around every and group.
Syntax
The pattern (A&B)|C allows any number of parenthesized intersection groups, but no nesting.
Benefit
No more artificial marker interface needed just to express a combined capability in a signature.
Limit
Intersection types work reliably only with interfaces, not with two unrelated concrete classes.