rfc:literal_scalar_types

PHP RFC: Literal Scalar Types

Introduction

PHP type declarations can name a scalar type such as int, float or string, but they cannot restrict a value to a specific member of that type. The true type and the earlier false type already allow a single boolean value to be used as a type. This RFC generalises that idea to integer, floating point and string literals, so that a parameter, property or return type can be limited to an exact set of values:

function setLogLevel('debug'|'info'|'warning'|'error' $level): void {}
function setSign(-1|0|1 $sign): int {}

Proposal

Integer, floating point and string literals may be used as a type, both standalone and as members of a union type. A value satisfies a literal type when it is equal to one of the listed literals, after applying the usual scalar coercion rules of the calling mode.

function f(1|2|3 $x): int { return $x; }
 
f(1); // int(1)
f(4); // TypeError: f(): Argument #1 ($x) must be of type 1|2|3, int given

Supported literals

  • Integer literals, including negative ones: 1, 0, -1.
  • Floating point literals, including negative ones: 1.5, -0.5.
  • Single or double quoted string literals. The two quoting styles are equivalent and escape sequences are resolved at compile time. Interpolation is not allowed; a double quoted string containing a variable is a parse error.

This RFC concerns integer, float and string literals only. true, false and null are already standalone types in their own right and are unrelated to this proposal.

Coercion

A literal type coerces exactly like its base scalar type. In coercive typing mode the value is coerced to the base type and then checked for membership:

function f(1|2|3 $x): int { return $x; }
 
f("2");  // int(2)
f(2.0);  // int(2)
f(true); // int(1)
f("4");  // TypeError, 4 is not a member of 1|2|3

When a literal is combined with a wider type, the literal member is preferred when the value matches it, otherwise the wider member applies:

function f(1|string $x) { return $x; }
 
f(1);   // int(1),    matches the literal
f("1"); // string("1"), matches the wider type
f(2);   // string("2"), coerced to string

Under strict_types=1 no coercion occurs, with the same single exception that already exists for the float type: an int is accepted where a float literal is expected.

declare(strict_types=1);
 
function f(1.5|2.0 $x): float { return $x; }
 
f(2);   // float(2)
f("2"); // TypeError

Redundancy checks

A union may not contain a literal that is already implied by another member. Both cases are compile time errors:

function f(1|1 $x) {}   // Literal type 1 is redundant as it is already present in the union
function f(1|int $x) {} // Literal type 1 is redundant as the union already allows its base type

Intersection types

A literal denotes a single value and carries no interface, so it cannot take part in an intersection type. 1&Foo is a parse error.

Default values

A default value must be equal to one of the type's literals:

function f(1|2 $x = 3) {} // Cannot use int as default value for parameter $x of type 1|2

Variance

A literal type is a subtype of its base scalar type, and a union of literals is a subtype of the union of the corresponding base types. The standard variance rules therefore apply: return types may be narrowed and parameter types may be widened.

class A {
    public function r(): int { return 1; }
    public function m(1|2 $x): void {}
}
 
class B extends A {
    public function r(): 1|2 { return 1; }   // ok, covariant return
    public function m(1|2|3 $x): void {}      // ok, contravariant parameter
}

Reflection

A new ReflectionLiteralScalarType class is added, extending ReflectionType and exposing the literal value:

class ReflectionLiteralScalarType extends ReflectionType
{
    public function getValue(): int|float|string {}
}

Standalone literals, including nullable ones, and the members of a literal union are reported as ReflectionLiteralScalarType. A union such as 1|2|'foo' is a ReflectionUnionType whose members are each a ReflectionLiteralScalarType. The inherited allowsNull() and __toString() behave as for any other type, rendering for example 1, ?1 or 1|2|'foo'.

Backward Incompatible Changes

None. Literal scalar types are a new construct and do not change the behaviour of any existing declaration. The only addition to userland is the ReflectionLiteralScalarType class.

Proposed PHP Version

PHP 8.6.

RFC Impact

To Reflection

Adds the ReflectionLiteralScalarType class described above.

To Opcache

None.

Proposed Voting Choices

Single yes/no vote, requiring a 2/3 majority to pass.

Patches and Tests

Implementation: php/php-src#22314. Tests live under Zend/tests/type_declarations/literal_types/.

References

rfc/literal_scalar_types.txt · Last modified: by azjezz