
前言先纠正版号纯交叉类型intersection types写作AB是 PHP 8.1 引入的不是 8.2。PHP 8.2 带来的其实是 DNF 类型Disjunctive Normal Form types也就是允许把交集和并集组合成(AB)|C这种形式。本文按事实讲AB从 8.1 起可用(AB)|C从 8.2 起可用。交叉类型的典型症状是这样你写了一个只接受既是Countable又是Traversable的参数接口本身没问题但一上线就收到TypeError而且传进去的对象看起来明明实现了这些接口。另一种情况是你想写可空的交叉类型写了?AB结果整个文件解析失败报错行号还指向别处。根因在于交叉类型的语义是同时满足而不是任一满足并且它对类型的种类有硬性限制只能由类名和接口名构成且不能出现冗余项。本文会从语义、语法限制、DNF 组合、实战与兼容性四个方面把它讲透所有示例最低版本 PHP 8.2可以直接存成.php运行。一、交叉类型的语义同时满足而不是任一满足AB读作A 且 B。传入的值必须同时是A的实例和B的实例。这在 PHP 里其实就是实现implements了这两个接口因为 PHP 不支持多继承function copyThrough(ReaderWriter $rw): void只有同时实现Reader和Writer的对象才能通过类型检查。传一个只实现Reader的对象会立刻抛TypeError。这一点和A|B完全相反写代码时很容易搞反写法语义传只实现 A 的对象引入版本A\B任一满足通过AB同时满足TypeErrorPHP 8.1交叉类型(AB)\C同时满足 A、B或者满足 CTypeError交叉类型可以出现在参数、返回值、属性三种位置这一点和联合类型一致。二、语法限制只能交集类/接口名交叉类型看似简单限制却不少而且大多在编译期就报错不会等到运行时成员只能是类名或接口名不能是int、string、array、bool这类标量类型不能包含null、false、true、mixed、void、never不能包含static也不能用self/parent去凑交集要写就写真实类名不能出现冗余项例如AA、或者子类与它已经实现的父接口同时出现都会报冗余错误?AB这种写法非法。?只能加在单一类型前面交集要变成可空必须用 DNF 或隐式可空交集里的每个名字都必须能被解析到use导入或写完整命名空间否则按类名解析很容易找不到类。在 PHP 8.1 上想要可空的交叉类型只能靠参数的隐式可空写法function f(ReaderWriter $rw null): void // PHP 8.1 可行但注意下面这段要特别提醒这种给参数默认值null从而隐式允许 null的写法在 PHP 8.4 起已被弃用官方建议显式写成?Type $x null单一类型或 DNF 形式(AB)|null交叉类型。所以如果你的库要长期维护可空交叉类型的正确出路是 8.2 的 DNF。三、DNF 类型PHP 8.2 的真正新东西DNF 允许在并集里放交集每个交集必须用括号包起来(CountableJsonSerializable)|null (AB)|(CD)|E规则很直白括号只能包住交集不能写成A(B|C)也不能嵌套成((AB)|C)D。并集依然是任一满足交集依然是同时满足。DNF 里同样禁止冗余类型。比如(AB)|A会被判定为冗余A已经覆盖了AB的全部实例编译期直接报错。这类报错在类型设计阶段就暴露出来其实是好事。四、实战与兼容性注意交叉类型最适合表达能力组合——一个方法需要对象既能读又能写、既能计数又能序列化。下面的例子把交叉类型和 DNF 都用上了?php // 最低版本PHP 8.2纯交叉类型 AB 需要 8.1DNF 类型 (AB)|C 需要 8.2 declare(strict_types1); interface Reader { public function read(): string; } interface Writer { public function write(string $data): void; } final class Buffer implements Reader, Writer { private string $data ; public function read(): string { return $this-data; } public function write(string $data): void { $this-data . $data; } } /** 同时要求 Reader 和 Writer只实现一个会 TypeError */ function copyThrough(ReaderWriter $rw, string $payload): string { $rw-write($payload); return $rw-read(); } final class Snapshot implements Countable, JsonSerializable { /** param liststring $items */ public function __construct(private array $items []) {} public function count(): int { return count($this-items); } public function jsonSerialize(): mixed { return $this-items; } } /** DNF 类型要么是可计数且可序列化的对象要么是 null */ function describe((CountableJsonSerializable)|null $value): string { if ($value null) { return null; } return json_encode($value, JSON_UNESCAPED_UNICODE) . / count . count($value); } echo copyThrough(new Buffer(), hello), PHP_EOL; // hello echo describe(new Snapshot([a, b])), PHP_EOL; // [a,b] / count2 echo describe(null), PHP_EOL; // null写库的人还要注意变型variance给参数加上交叉类型等于收紧了入参——原来能传的对象现在可能不行了。这是对已有 API 的破坏性变更属于语义化版本里的大版本改动。返回类型上加交集则会放宽调用方的假设相对安全。常见坑点1. 把交叉类型当成取并集❌function handle(ReaderWriter $x)之后传一个只实现了Reader的类期望它至少能当Reader用。 ✅ 需要任一就用Reader|Writer需要同时才用并且给TypeError准备好清晰的报错提示。2. 写?AB表示可空交叉类型❌function f(?CountableJsonSerializable $x)——PHP 8.1 和 8.2 都是解析错误报错位置往往指向你完全没动过的一行。 ✅ PHP 8.2 起写(CountableJsonSerializable)|null8.1 上只能靠AB $x null但要知道这个写法在 8.4 已弃用。3. 交集里塞标量或null❌function f(stringCountable $x)或Anull——编译/解析阶段直接失败。 ✅ 交集成员只能是类名或接口名标量和null放进并集或 DNF 里。4. 写出冗余交集❌function f(CountableCountable $x)、(AB)|A或把父接口和它已覆盖的子接口一起写进去。 ✅ 用ReflectionClass::getInterfaceNames()或静态分析工具先理清继承关系再写交集。5. 在 8.1 上用 DNF 语法❌ 环境是 8.1代码里写(AB)|null——解析失败整个文件加载不了class_exists()都返回 false排查时容易误判成自动加载问题。 ✅ 在composer.json里写php: 8.2并让 CI 在最低版本上跑一遍语法检查。6. 属性上的交叉类型没初始化❌class C { public ReaderWriter $rw; }后直接读$c-rw——属性未初始化抛Error: Typed property must not be accessed before initialization。 ✅ 用构造函数属性提升赋值public function __construct(public ReaderWriter $rw) {}。7. 忘记导入命名空间交集被当成两个全局类❌ 文件顶部没use App\Contracts\Writer;类型写成ReaderWriter——PHP 会去找全局的Writer类报class not found而你的 IDE 看着一切正常。 ✅ 每个交集成员都显式use或写完整限定名。总结类型写法语义引入版本典型场景A\B任一满足PHP 8.0AB同时满足PHP 8.1既能读又能写的能力组合(AB)\CDNF 组合PHP 8.2(AB)\null可空交叉PHP 8.2交叉类型把这个参数必须同时具备多种能力从注释搬进了类型系统代价是它对成员种类和冗余非常严格。规范用法可以归纳成一句先确认最低版本AB是 8.1、DNF 是 8.2再用交集表达且、并集表达或可空一律显式写进 DNF最后记得导入每一个类名。