How the New Intersection Types in PHP 8.1 Give You More Flexibility – CloudSavvy IT


    PHP Logo

    Intersection types are a new type system feature coming in PHP 8.1. They let you typehint values that must satisfy more than one type constraint. PHP’s already got union types that combine types with a logical “or” clause; intersection types offer an “and” clause instead.

    Some developers already typehint intersections using PHPDoc annotations. The addition of native support is a step forward as the types will actually be enforced at runtime. They’re also fully compatible with class inheritance, helping reduce the risk of errors due to outdated or incompatible annotations.

    Basic Syntax

    The intersection type syntax is similar to union types:

    class DemoClass {
     
        public Countable|Stringable $Union;
     
        public Countable&Stringable $Intersection;
     
    }

    In this example, $Union will be satisfied by any value which is either Countable or Stringable. $Intersection will only accept values that meet both the constraints. You’ll get a TypeError if you assign the property a value that implements one or zero of the hinted types.

    Intersection types are supported everywhere that typehints work. You can use them with property definitions, function parameters, and return values.

    Unlike union types, intersections can only typehint interfaces and classes. A scalar intersection like int&string is meaningless as it could never be satisfied. The mixed type is banned too, because every value will satisfy its constraint.

    Type Variance

    Intersection types respect PHP’s existing variance rules. The return types of overridden methods must be covariant, while parameter types are contravariant. Class properties are always invariant, so children cannot change the type definition unless the new signature is both a sub-type and super-type of the inherited one.

    For return values, the variance rules mean you can add additional intersection types in overridden methods. Methods may also remove, but not add, types to intersections used as parameters. These rules enforce the Liskov substitution principle.

    interface I1 {
        public function method1(A&B $demo) : X&Y;
        public function method2(A&B $demo) : X&Y;
    }
     
    interface I2 extends I1 {
     
        // ALLOWED
        public function method1(A $demo) : X&Y&Z;
     
        // NOT ALLOWED
        public function method2(A&B&C $demo) : X;
     
    }

    In the case of method1, the constraints haven’t actually changed. You’re stating the overridden method can actually work with any A, even if it’s not also an X. It’s less specific which results in acceptable parameter variance. The return declaration is more specific, stating the value will implement X, Y and Z; in this scenario, adding specificity does not break the contract, as the value will still be accepted by anything that typehints I1.

    The method2 override is broken on both counts. By requiring the input parameter satisfy A, B and C, it’s no longer a drop-in replacement for I1. Similarly, method2 only guarantees the return value will be an instance of X. This breaks the contract as anything typehinting I1 requires the value to satisfy the intersection of X and Y.

    Intersections invert the practical variance rules of union types. As unions are combined using “or,” children can add parameter types and remove return types. The Liskov substitution principle is satisfied as the widening and narrowing effect of the type constraints is reversed.

    As with union types, variance rules also apply to the types that make up an intersection – that’s the individual X and Y parts of X&Y. You may narrow a type when returning it – stating you’ll return a sub-class – or widen it as a parameter, accepting a super-class.

    Intersections also possess some special rules around aliasing and concrete implementations. If you typehint X&Y but write an interface Z extends X, Y, it’s logical that Z satisfies the constraint. Consequently, you can typehint Z instead of X&Y wherever co-variance is allowed:

    interface Test extends X, Y {}
     
    interface Demo1 {
        public function demo() : X&Y;
    }
     
    interface Demo2 {
        public function demo() : Test;
    }

    This lets you typehint a concrete class or interface if you depend on additional functionality. It’s acceptable as long as all the constraints in the intersection are satisfied by the final typehint.

    When to Use Intersection Types?

    Intersection types are for the times when you want to be sure a value satisfies a compound interface without actually defining that interface. Previous PHP versions didn’t provide any native support for this, so you needed to add a mess of extra boilerplate to your codebase:

    interface CountableStringable extends Countable, Stringable {
        // ...
    }
     
    class FakeArray implements CountableStringable {
     
        public array $items = [];
     
        public function count() : int {
            return count($this -> items);
        }
     
        public function __toString() : string {
            return implode(", ", $this -> items);
        }
     
    }
     
    class DemoClass {
     
        public CountableStringable $Value;
     
    }

    This leads to an excess of stub-like shallow interfaces to achieve basic intersection behavior. Although PHP developers have survived without intersections to date, their presence helps solidify the type system’s compound capabilities. Using native intersections creates cleaner and more intuitive code.

    What About Composite Types?

    It’s not possible to combine intersection and union types in the same typehint. While it would be technically possible, it’s been omitted from the current RFC as there are ambiguities around syntax, precedence, and variance.

    Composite types remain an idea for the future. If they were added, you could start to add complex typehints like this:

    class DemoClass {
     
        public Countable&Stringable|CountableStringable $Intersection;
     
    }

    This example class combines the two implementations from the above. If it worked, it would let you adopt native intersection types while retaining backward compatibility with old classes using the “fake” approach.

    Fully-fledged composite types would round off the multiple type handling facilitated by unions and intersections. In the meantime, you’ll need to keep writing your own composite interfaces in these scenarios.

    Summary

    Intersection types are coming to PHP 8.1 and will unlock more advanced possibilities within the type system. Extending the options around composite types reduces the amount of code you need to write when multiple interfaces are supported.

    Intersections are an optional feature that will not introduce any incompatibilities with existing code. The RFC was implemented in the third PHP 8.1 alpha build. The final release will arrive in late November later this year.



    Source link