Dreaming of a strong and statically-typed PHP

6 Jan 2024

PHP has historically been a dynamically-typed language, but with the introduction of various type declarations, could we dream up a future where PHP is both strong and statically-typed?

PHP has historically been a dynamically-typed language, but with the introduction of various type declarations, could we dream up a future where PHP is both strong and statically-typed?

Since the release of PHP 7.0 back in December 2015, PHP has continued to introduce new features to the type system. The latest release allows us to type parameters, return types, and properties.

These changes have helped us write better code with type safety and also improved tooling, but there are still some things that I think could be introduced (to the language or ecosystem) to bring PHP up to the same level as other modern languages.

Variable types

We can declare types for function parameters, which are variables. We can declare types of class properties, which are also a form of variable. But we can't yet declare the types of regular variables, the ones we use day-in, day-out.

Having the ability to type variables greatly improves type safety by ensuring a variable can only ever be the correct type.

I think the biggest problem surrounding this is the syntactical ambiguity. The most ideal syntax would be one that is C-like.

string $message;

This syntax is identical to how properties and parameters are typed, but it is potentially problematic when it comes to parsing since there are other expressions in the language that also look similar, such as include & require.

The other two syntax ideas are:

var string $message;

The var keyword isn't new, it's actually from an older version of PHP and is analogous to a public property on a class. This is a viable candidate.

The other option follows the same syntax as return types on functions and methods, and is similar to a lot of other modern languages like TypeScript and Rust.

$message: string;

Visually this kind of looks like a label, but a parser should be able to handle this quite easily.

In my opinion, the safest route would be using the var syntax. It's unambiguous, easy to implement in the parser, and also makes it visually clear that this variable is going to be typed. It could also double as a "pre-defined" variable setup, allowing you to create an empty / unset variable prior to assignment.

Variable types would also start to bring in the concept of strong typing. If a variable is said to be of a particular type, any future assignments to the variable must also be of the same type, as opposed to the current "loose" typing mechanisms that PHP employs. It's totally opt-in too, just like the rest of the type system.

Typed Arrays

I don't want to dive into the generics discussion too much in the post, since there's a lot of controversy surrounding it, but I think the most minimal form of generics – typed arrays – could be a huge win for the language.

We do already have support for these at the DocBlock level, if paired with a static analyser, but if PHP introduced first-party support for them, it could be even better. It's probably one of the most common usages of a DocBlock (although I don't have any evidence to back this up)!

The syntax here is also kind of up for discussion. Static analysers support the T[] notation, but there's also the array<T> form. The latter is more generic-esque, but without support for other forms of generic types, i.e. T<U>, it kind of feels strange to have in the language.

The former does become kind of annoying to write though when you have a union of types inside of an array since you need to wrap the type in parentheses (T | U)[].

There is actually a language feature that kind of already supports typed arrays, that is "typed variadic parameters".

declare(strict_types=1);

function foo(int|string ...$args)
{
    // ...
}

foo(1, true, "bar");

This will actually throw, because true doesn't fit into the int|string type. I'm not entirely sure how these work under the hood, so can't speak to the complexity of expanding it into support for typed arrays, but conceptually it's a very similar thing.

Type aliases

I've spoken about this one before on Twitter and in other posts, but now that PHP has support for union, intersection, and DNF types, there are lots of cases where property and method signatures start to overlap and feel a bit "bloated" for want of a better word.

A good example is in the Filament codebase – a small trait that is used across a bunch of different classes.

trait HasColor
{
    protected string | array | Closure | null $color = null;

    protected string | array | Closure | null $defaultColor = null;

    public function color(string | array | Closure | null $color): static
    {
        $this->color = $color;

        return $this;
    }
}

The string | array | Closure | null type appears three times in this code snippet. If I want to add a new type, I need to update all 3 places.

If type aliases were introduced, this could be abstracted out into a "named" type alias, such as ColorValue.

type ColorValue = string | array | Closure | null;

trait HasColor
{
    protected ColorValue $color = null;

    protected ColorValue $defaultColor = null;

    public function color(ColorValue $color): static
    {
        $this->color = $color;

        return $this;
    }
}

Immediately this becomes super clear. With solid IDE tooling, you can just click through to the type alias and find the "real" underlying type. Tooling could also expand the alias for you when hovering over a method or property from somewhere else.

I think there are a few things that need to be ironed out to nail this feature though:

  1. Are type aliases purely local to their file, i.e. I can only use ColorValue inside of the file it's defined in.
  2. If they're not local, are they autoloadable?
  3. Can they be taken a step further and scoped down to specific structures, such as a class a la Rust.

If that can be figured out, I think type aliases would be an excellent addition to PHP's type system in a future release. I'm actually tempted to draft up an RFC myself and try to implement them!