There are some issues I would like to address. Union types are necessary, but I believe that some improvements must be made to this proposal.
Type aliasing
Let's look at the following example.
use Foo\bar; function test(bar $bar) { // is $bar an object? }
If the Foo\bar type exists, then $bar is definitely an object. This is how it's been for years. But now, with the introduction of exported types, $bar could be anything. And this incertitude could prove to be a PITA in the long run.
namespace Foo; type bar = int|float; // Usable as \Foo\bar from elsewhere
Proposal
Introduce use type. This way we could no longer make wrong assumptions.
use Foo\bar; use type Foo\baz; use type int|float|string|bool as scalar; // local type function test1(baz $baz, scalar $scalar) { // Don't make assumptions, check type definition. } function test2(bar $bar) { // It's an object }
Nullable types
Nullable types should not be allowed when defining a union type.
use type ?(T1|T2|T3) as foo; use type bar; // What's this? Nullable nullable type? function test(?foo $val) { } // Is bar nullable? function test2(bar $val) { // Should I check if $val is null? if (is_null($val)) { } }
Proposal
Keep it simple. No nullable types inside unions
use type T1|T2|T3 as foo; // Not nullable function test(foo $val) { // $val can't be null } // Nullable function test2(?foo $val) { // I should check if $val is null if (is_null($val)) { } }
Keep the syntax clean
There should be a single way of declaring a union type. The in place way of declaring a union type should be removed.
use type float|int as number; // Confusing.. function sum(float|int $a, int|float $b): number{ } // ?? function example(int|float $value, int $flag = 2|4|8|16|32, string|object $item = null): float|int|false { }
It's hard to read, hard to follow and the syntax is super ugly.
Proposal
Keep the syntax unchanged by forcing the user to either export a union type or declare it locally with the help of use type.
use type float|int as number; // Makes sense now.. function sum(number $a, number $b): number{ }
This way the ReflectionUnionType is no longer necessary. Only the ReflectionType needs to be changed.
class ReflectionType { isUnionType(): bool; getUnionTypes(): array; }
Putting all together..
type scalar=int|float|string|bool; // export union type use type int|float as number; // local union type class Foo { private number $foo; // non-nullable property private ?number $bar; // nullable property // non-nullable arguments & return type function sum(number $a, number b):number { } // nullable arguments and return type function test(?scalar $value): ?scalar { } }