How and When to Use PHP’s Pure Intersection Types

intersection
Intersecting Roads @Bing Picture Creator

In this publication of my PHP related blog series, I want to address Pure Intersection Types in PHP. In the previous blogs of the series, I addressed different features added to PHP with PHP 8 and PHP 8.1. The first blog was about readonly properties, the second one focused on Enums and the third and so far last one addressed Fibers in PHP.

These posts are not dependent on each other. You do not have to read the previous ones to understand the current one. However, if you are interested in PHP related topics, I highly recommend to read them as well 🙂

What are “Pure Intersection Types”

Pure Intersection Types, introduced in PHP 8.1, are a language feature that enables developers to express more precise type constraints for function parameters, return types, and class properties. They allow specifying that a value must belong to multiple types at the same time, rather than just one type as in the case of Union Types.

The syntax for Intersection Types uses the ‘&’ symbol to combine types, for example:

In this example, the $arg parameter must be an instance of both classes A and B. If it does not meet these requirements, a TypeError will be thrown.

The addition of Pure Intersection Types to PHP was driven by several key motivations:

  1. Improved Type Safety: Intersection Types enable developers to express more precise type constraints, reducing the risk of type-related errors at runtime. This results in more robust and reliable code.
  2. Enhanced Code Readability: With more accurate type information, the code becomes easier to understand and maintain. Developers can quickly grasp the requirements of a function or a class property, leading to increased productivity.
  3. Better IDE and Tooling Support: Intersection Types help IDEs and other development tools to provide more accurate autocompletion, type checking, and code analysis. This improves the overall development experience and reduces the likelihood of introducing type-related bugs.
  4. Facilitating Dependency Injection: Intersection Types are particularly useful in scenarios where multiple interfaces need to be implemented by a single object, such as in Dependency Injection (DI) systems. They provide a more concise and expressive way to define such requirements.

Pure Intersection Types may provide developers with a powerful way to express type constraints, making PHP code more reliable and maintainable.

Use Cases for Pure Intersection Types

Of course, there are several use cases. And not all make sense in every situation. But in general, I would advice to not use the feature too much or everywhere. Some use cases are quite useful – especially if you work with public interfaces for others (libraries, for example).

One practical example is with Keestash, where we implemented an EventDispatcher. Among other things, the EventDispatcher class calls listener classes for events that got triggered. The IListener interface is as followings:

As you can see, the execute method of IListener class gets an IEvent object passed as an argument. IEvent is a loose and abstract event class, which does not hold any values.

However, implementing event classes, have sometimes arguments which are needed in the listener class in order to process the event. But having IEvent as an argument only, you have to check for the concrete implementation with the instanceof operator. With Pure Intersection Types, you can do the following:

Notice that there are other ways to solve this issue as well. However, this is just a demonstrating example.

“Good to Know”s about Pure Intersection Types

Pure Intersection Types have more important points to consider while working with them:

  1. No duplicates allowed: A Pure Intersection Type cannot contain duplicate types. For example, A & A would be invalid. This is because duplicates would not provide any additional value and may introduce confusion.
  2. No support for mixed type: Intersection Types do not support the mixed type, which represents any value. Including mixed would essentially make the intersection type redundant since it already covers all possible types.
  3. Covariance and contravariance: When using Pure Intersection Types with inheritance or interface implementation, it is crucial to understand PHP’s rules for covariance and contravariance. Return types are covariant, meaning that a subclass or implementing class can have a more specific return type. On the other hand, parameter types are contravariant, which means that a subclass or implementing class can accept a more general parameter type.
  4. No support for nullable intersection types: You cannot use the ? symbol to denote a nullable intersection type directly. To achieve this, you need to use a union type in conjunction with the intersection type, for example: A & B | null.
  5. No intersection of scalar types: Intersection Types do not support the intersection of scalar types (e.g., int & float). This is because scalar types are mutually exclusive and cannot overlap.

Summary

If you read my blog, you will have noticed that I am not a fan of inheritance, Traits, loosely coupled (smelly) code and everything what may lead to ambiguity. However, being tooooooo strict leads sometimes to overengineered code, which does not make sense (at least not in every case).

Therefore, in contrast to Traits or inheritance, I recommend to evaluate critically if there is not a good solution without using constructs like Pure Intersection Types. If you see yourself doing several workarounds or write boilerplate code, stop doing this and check how to implement features like Pure Intersection Types as clean as possible.