In PHP 8.2+, what does a DNF type such as (HasEmail&HasConsent)|null express, and which intersection and DNF forms does PHP reject?
answer
- A&B: must satisfy every type, 8.1
- class and interface names only
- DNF: OR of parenthesised ANDs, 8.2
- A&(B|C) does not parse
- (A&B)|A is redundant
basics
~20 sA&B, an intersection type (PHP 8.1), accepts only objects that are instances of every listed class or interface. DNF types (PHP 8.2) combine them with unions as an OR of parenthesised ANDs, so (HasEmail&HasConsent)|null means an object implementing both interfaces, or null.
solid answer
~40 sAn **intersection type** `HasEmail&HasConsent` (PHP 8.1) requires the value to satisfy **all** listed types; only class and interface names may take part, so `int&string`, `self`, `parent` and `static` are rejected. PHP 8.1 had no way to make an intersection nullable. **PHP 8.2** added **Disjunctive Normal Form** types: a union whose members may be parenthesised intersections, such as `(HasEmail&HasConsent)|null` or `(HasEmail&HasConsent)|Lead`. The form is strict: an OR of ANDs only, so `HasEmail&(HasConsent|Lead)` does not parse and must be rewritten as `(HasEmail&HasConsent)|(HasEmail&Lead)`; `?` cannot prefix an intersection. The compiler also rejects provable redundancy: `(A&B)|A` is redundant because anything matching `A&B` already matches `A`. In practice these types let a CRM mailer demand a contact that has an email address **and** recorded consent, without inventing a combined interface.
code
php · 24 lines<?php
declare(strict_types=1);
interface HasEmail { public function email(): string; }
interface HasConsent { public function consentsToMarketing(): bool; }
final class Lead implements HasEmail, HasConsent
{
public function __construct(private string $email, private bool $optIn) {}
public function email(): string { return $this->email; }
public function consentsToMarketing(): bool { return $this->optIn; }
}
function firstReachable(array $contacts): (HasEmail&HasConsent)|null
{
foreach ($contacts as $c) {
if ($c instanceof HasEmail && $c instanceof HasConsent && $c->consentsToMarketing()) {
return $c;
}
}
return null;
}
var_dump(firstReachable([new Lead('[email protected]', true)]) instanceof Lead); // bool(true)go deeper
Recall that A&B means an object must be an instance of both types, and that only classes and interfaces can appear in it.
Explain the 8.1 limitation, the 8.2 DNF syntax with parentheses, and why ? cannot be used with intersections.
Use intersections for capability combinations at boundaries, rewrite AND-of-OR requirements into DNF by hand, and prefer a named interface when the combination repeats.
Weigh ad hoc intersection types against named composite interfaces as a public API grows, considering readability and how contracts are documented.
## Intersection types: all of these A union `A|B` accepts a value that matches **any** member. An **intersection** `A&B`, added in **PHP 8.1**, accepts a value that matches **every** member. Because a value can only be several types at once if it is an object implementing several interfaces (or extending a class and implementing interfaces), intersections are limited to class and interface names. In a CRM, a campaign mailer may need a contact that exposes an email address and also carries marketing consent. Some contacts are customers, some are leads; both classes implement the two interfaces independently: ```php <?php declare(strict_types=1); interface HasEmail { public function email(): string; } interface HasConsent { public function consentsToMarketing(): bool; } function sendCampaign(HasEmail&HasConsent $contact): void { if ($contact->consentsToMarketing()) { mail($contact->email(), 'Autumn offer', '...'); } } ``` Without intersections the alternatives were a third interface extending both (which every class must opt into) or a docblock the engine ignores. ## What an intersection may contain - **Class and interface names only.** `int&string` is rejected (`Type int cannot be part of an intersection type`), and so are `callable` and `iterable`. - **Not `self`, `parent` or `static`.** - **No duplicates**: `HasEmail&HasEmail` is redundant. - **No `?` prefix.** `?HasEmail&HasConsent` does not parse. ## DNF types: unions of intersections PHP 8.1 intersections could not be combined with anything, which made a common need impossible: "such a contact, **or null**". **PHP 8.2** added **Disjunctive Normal Form (DNF) types** — a union whose members are single types or **parenthesised intersections**: ```php <?php function nextRecipient(): (HasEmail&HasConsent)|null { return null; // queue empty } ``` "Disjunctive normal form" is a term from logic: an OR of ANDs. PHP requires the declaration to be **already** in that form; it will not normalise it for you. | Declaration | Result | |---|---| | `(HasEmail&HasConsent)\|null` | valid (8.2+) | | `(HasEmail&HasConsent)\|Lead` | valid | | `(A&B)\|(A&C)` | valid | | `HasEmail&(HasConsent\|Lead)` | parse error: AND of an OR | | `?(HasEmail&HasConsent)` | parse error: use `\|null` | | `(A&B)\|A` | compile error: `A&B` is more restrictive than `A` | | `(A&B)\|(B&A)` | compile error: the same intersection twice | To express "has email, and is either consenting or a lead", distribute by hand: `(HasEmail&HasConsent)|(HasEmail&Lead)`. ## How the check runs At call time PHP tests each union member in turn; a parenthesised intersection matches only if the object is an instance of every name inside it. As with unions, the compile-time redundancy check does not load classes, so it only catches what it can see from names alone — a `(A&B)|C` where `C` extends `A` and implements `B` still compiles. ## When to use them 1. **Capability combinations** at a boundary: "anything that is both `HasEmail` and `HasConsent`", when the implementing classes are unrelated and you do not want a combined interface. 2. **Optional capability objects**: `(A&B)|null` for a lookup that can miss. 3. **Not as a substitute for design.** If the same intersection appears in many signatures, a named interface that extends both is usually clearer to read and to document. These types are rarely needed day to day; interviewers use them to check that a candidate can read a precise native contract and knows the 8.1 and 8.2 boundaries. ## Common mistakes - **Reading `&` as "either".** `HasEmail&HasConsent` requires both; an object implementing one of them fails with `TypeError`. - **Trying scalar intersections.** No value is both an `int` and a `string`, and PHP rejects the declaration rather than accepting an impossible type. - **Writing `?` on an intersection.** On PHP 8.1 there was no nullable intersection at all; on 8.2 and later the form is `(A&B)|null`. - **Expecting normalisation.** PHP does not rewrite `A&(B|C)` into DNF; the declaration must already be an OR of parenthesised ANDs. - **Confusing the parameter `&` with by-reference passing.** In a declaration, `A&B $x` is an intersection type, while `A &$x` passes `$x` by reference; the lexer tells them apart by whether a variable follows the ampersand.
- In PHP 8.2+, how do you declare a parameter that must implement HasEmail and also be either HasConsent or Lead?Distribute it into disjunctive normal form yourself: `(HasEmail&HasConsent)|(HasEmail&Lead)`. PHP only accepts an OR of parenthesised ANDs, so `HasEmail&(HasConsent|Lead)` is a parse error even though it means the same thing logically.
- In PHP 8.1, how could you accept an intersection type or null before DNF types existed?Natively, you could not: 8.1 intersections could not be combined with `null` or any union. Code either dropped the declaration, declared a named interface extending both and made classes implement it, or accepted one interface and checked the other with `instanceof` in the body. PHP 8.2's `(A&B)|null` removed the need.
An intersection is a job posting that requires both a driving licence and a first-aid certificate; a DNF type is a posting that accepts either a candidate holding both certificates or no candidate at all, and it must be written as that list of complete requirement sets.
saying these in an interview costs you the question
- Says an intersection like int&string can describe a value that is both.
- Writes ?(A&B) or ?A&B to make an intersection nullable.
- Expects PHP to accept A&(B|C) and normalise it automatically.
- Thinks intersection types were available together with unions from PHP 8.0.
- Believes (A&B)|A is fine because it is merely redundant.