Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
On Mon, Jun 15, 2026 at 2:24 AM Seifeddine Gmati azjezz@carthage.software
wrote:
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
Prima facie, I feel like enums already do what this aims to achieve, much
better.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.Prima facie, I feel like enums already do what this aims to achieve, much better.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
Hi David,
I agree enums are the better fit for a lot of cases. but not all.
- describing APIs that already exist.
array_filter's$modereally
accepts0|1|2, but it's typedintbecause that's all the type
system can say today. we can't retype it as an enum without breaking
every caller. a literal union lets the signature state the actual
contract. - ad-hoc / open value sets. for a library, "ascii"|"utf-8" would need
its own named symbol (enum BorderStyle { case Ascii; case Utf8 }, a
new file, an import ) for what is really two strings. and because an
enum is a closed set, adding a third style later breaks any consumer
that match-es over it without a default. widening the union on a
parameter ( "ascii"|"utf-8"|"unicode" ) is contravariant, so it breaks
nobody. - scalar interop. a literal value is the scalar, so it works as an
array key, compares with ===, round-trips through json, etc. enum
cases are objects and don't.
so they overlap a lot, but literal unions reach things enums
structurally can't: existing scalar APIs, and open sets that grow
without a BC break.
Cheers,
Seifeddine.
On Mon, Jun 15, 2026 at 3:13 AM Seifeddine Gmati azjezz@carthage.software
wrote:
On Mon, Jun 15, 2026 at 2:24 AM Seifeddine Gmati
azjezz@carthage.software wrote:Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.Prima facie, I feel like enums already do what this aims to achieve,
much better.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.Hi David,
I agree enums are the better fit for a lot of cases. but not all.
- describing APIs that already exist.
array_filter's$modereally
accepts0|1|2, but it's typedintbecause that's all the type
system can say today. we can't retype it as an enum without breaking
every caller. a literal union lets the signature state the actual
contract.
This is probably the strongest case (and should be mentioned on the RFC, I
think), though I'm not sure it's a sufficient justification for the scope
of change.
- ad-hoc / open value sets. for a library, "ascii"|"utf-8" would need
its own named symbol (enum BorderStyle { case Ascii; case Utf8 }, a
new file, an import ) for what is really two strings. and because an
enum is a closed set, adding a third style later breaks any consumer
that match-es over it without a default. widening the union on a
parameter ( "ascii"|"utf-8"|"unicode" ) is contravariant, so it breaks
nobody.
I'm not so convinced on this point. You add a new case to an enum, that
library's API isn't inherently broken. Users passing Ascii or Utf8 per the
original case-set remain valid. The only code that breaks there is code
that assumes the enum would never gain another case and the same could be
said of code matching on string literals without a default. And if
"ascii"|"utf-8" is what a library exposes, consumers may treat that as
exhaustive, whereas an enum doesn't inherently break as a type hint. A set
that's expected probably shouldn't be an enum, but the strength of an
enum is precisely that it's a (probably) closed set of values. On the
flip-side, if a set of values is genuinely open-ended, a closed set of
scalar literal unions as a type isn't going to help.
- scalar interop. a literal value is the scalar, so it works as an
array key, compares with ===, round-trips through json, etc. enum
cases are objects and don't.
I think backed enums already cover this through value exposure.
so they overlap a lot, but literal unions reach things enums
structurally can't: existing scalar APIs, and open sets that grow
without a BC break.Cheers,
Seifeddine.
On Mon, Jun 15, 2026 at 3:13 AM Seifeddine Gmati azjezz@carthage.software
wrote:On Mon, Jun 15, 2026 at 2:24 AM Seifeddine Gmati
azjezz@carthage.software wrote:Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.Prima facie, I feel like enums already do what this aims to achieve,
much better.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.Hi David,
I agree enums are the better fit for a lot of cases. but not all.
- describing APIs that already exist.
array_filter's$modereally
accepts0|1|2, but it's typedintbecause that's all the type
system can say today. we can't retype it as an enum without breaking
every caller. a literal union lets the signature state the actual
contract.This is probably the strongest case (and should be mentioned on the RFC, I
think), though I'm not sure it's a sufficient justification for the scope
of change.
- ad-hoc / open value sets. for a library, "ascii"|"utf-8" would need
its own named symbol (enum BorderStyle { case Ascii; case Utf8 }, a
new file, an import ) for what is really two strings. and because an
enum is a closed set, adding a third style later breaks any consumer
that match-es over it without a default. widening the union on a
parameter ( "ascii"|"utf-8"|"unicode" ) is contravariant, so it breaks
nobody.I'm not so convinced on this point. You add a new case to an enum, that
library's API isn't inherently broken. Users passing Ascii or Utf8 per the
original case-set remain valid. The only code that breaks there is code
that assumes the enum would never gain another case and the same could be
said of code matching on string literals without a default. And if
"ascii"|"utf-8" is what a library exposes, consumers may treat that as
exhaustive, whereas an enum doesn't inherently break as a type hint. A set
that's expected probably shouldn't be an enum, but the strength of an
enum is precisely that it's a (probably) closed set of values. On the
flip-side, if a set of values is genuinely open-ended, a closed set of
scalar literal unions as a type isn't going to help.
Apologies for typo of omission in the above paragraph, I meant to say "A
set that's expected to grow probably shouldn't be an enum"
- scalar interop. a literal value is the scalar, so it works as an
array key, compares with ===, round-trips through json, etc. enum
cases are objects and don't.I think backed enums already cover this through value exposure.
so they overlap a lot, but literal unions reach things enums
structurally can't: existing scalar APIs, and open sets that grow
without a BC break.Cheers,
Seifeddine.
Hi
- describing APIs that already exist.
array_filter's$modereally
accepts0|1|2, but it's typedintbecause that's all the type
system can say today. we can't retype it as an enum without breaking
every caller. a literal union lets the signature state the actual
contract.
We can retype this kind of API with enums.
See the “Correctly name the rounding mode and make it an Enum” RFC
(https://wiki.php.net/rfc/correctly_name_the_rounding_mode_and_make_it_an_enum)
for an example: We first widen the parameter to accept the enum so that
folks can opt-in to the new API. At a later point we alias the constants
to the corresponding enum cases and deprecate passing the integers and
then we remove the support for the integers (and constants).
Using literal types is going to result in a terrible user-experience,
because the signature does not provide any hint as to which constants
are supposed to be used with the API which means that the resulting
error message is also useless to the user. Enums - or the existing
manual validation - is much preferable here.
- ad-hoc / open value sets. for a library, "ascii"|"utf-8" would need
its own named symbol (enum BorderStyle { case Ascii; case Utf8 }, a
new file, an import ) for what is really two strings. and because an
enum is a closed set, adding a third style later breaks any consumer
that match-es over it without a default. widening the union on a
parameter ( "ascii"|"utf-8"|"unicode" ) is contravariant, so it breaks
nobody.
The existing \RoundingMode enum is already intended to be a
non-exhaustive (parameter-only) enum where users are expected to include
a default case in case new values are being added.
I have a very rough draft in
https://wiki.php.net/rfc/non_exhaustive_marker to make this type of
contract more explicit.
Having an “own named symbol” for the allowed values is a benefit to me,
because this makes it easy to reuse the list of allowed values in
different locations without needing to resort to copy and paste, for
example in decorators that just pass through the values without touching
them.
- scalar interop. a literal value is the scalar, so it works as an
array key, compares with ===, round-trips through json, etc. enum
cases are objects and don't.
Enums can be compared with ===.
Best regards
Tim Düsterhus
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
I think I'm okay with this. David mentioned enums, and I do think enums
are useful in many places where you want types like this, but there's a
simplicity in this that I can't deny, and I like it.
Cheers,
Ben
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.I think I'm okay with this. David mentioned enums, and I do think enums
are useful in many places where you want types like this, but there's a
simplicity in this that I can't deny, and I like it.
I do like the simplicity of this.
But - especially for floats and ints - the next level would be to allow
not only
public function check(int -1|0|1 $minusOneThroughOne)
but also something like
public function check(int -1..1 $minusOneThrougOne)
which would then also allow
public function check (int 1..PHP_INT_MAX $positiveInt)
would that also be something to be considered? It seems like a logical
alternate option to not have to add every value literally to the option
list...
Cheers
Andreas
,,,
(o o)
+---------------------------------------------------------ooO-(_)-Ooo-+
| Andreas Heigl |
| mailto:andreas@heigl.org N 50°22'59.5" E 08°23'58" |
| https://andreas.heigl.org |
+---------------------------------------------------------------------+
| https://hei.gl/appointmentwithandreas |
+---------------------------------------------------------------------+
| GPG-Key: https://hei.gl/keyandreasheiglorg |
+---------------------------------------------------------------------+
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.I think I'm okay with this. David mentioned enums, and I do think enums
are useful in many places where you want types like this, but there's a
simplicity in this that I can't deny, and I like it.I do like the simplicity of this.
But - especially for floats and ints - the next level would be to allow
not onlypublic function check(int -1|0|1 $minusOneThroughOne)but also something like
public function check(int -1..1 $minusOneThrougOne)which would then also allow
public function check (int 1..PHP_INT_MAX $positiveInt)would that also be something to be considered? It seems like a logical
alternate option to not have to add every value literally to the option
list...Cheers
Andreas
,,, (o o)+---------------------------------------------------------ooO-(_)-Ooo-+
| Andreas Heigl |
| mailto:andreas@heigl.org N 50°22'59.5" E 08°23'58" |
| https://andreas.heigl.org |
+---------------------------------------------------------------------+
| https://hei.gl/appointmentwithandreas |
+---------------------------------------------------------------------+
| GPG-Key: https://hei.gl/keyandreasheiglorg |
+---------------------------------------------------------------------+
Hi Andreas,
Thanks! Your first example, -1|0|1, is already exactly what this RFC
does: a union of three int literals (you don't even need the int
prefix, just function check(-1|0|1 $x): void {}).
The second part, ranges like -1..1 or 1..PHP_INT_MAX, is a
genuinely different feature. And you're right that it's the logical
next step: enumerating every value doesn't scale, and something like
1..PHP_INT_MAX can't be written as a union at all.
I'd keep it out of this RFC, though. A range isn't a set of literals,
it's a constraint that has to be checked with a bounds predicate ($v
= lo && $v <= hi), and it brings its own design questions: inclusive
vs exclusive bounds, open-ended ranges (1.., ..10), what's allowed as
a bound (plain constants? expressions like PHP_INT_MAX?), and how
coercion behaves at the edges. That's really a refinement-type feature
sitting on top of this one.
So it's a great follow-up, and a natural one once literals exist, but
I think it deserves its own proposal rather than being folded in here.
Cheers,
Seifeddine
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
I mainly see the benefit here as being able to be more strict about what
a function actually accepts and returns, in cases where a dedicated enum
would be overkill.
Two things I'd like to understand better:
Does the RFC allow referencing constants in type positions, or only raw
literal values?
function foo(STATUS_ACTIVE|STATUS_INACTIVE $sort): void {}
What about enum values? For example:
function bar(Status::Active->value $status): void {}
// or simply as
function bar(Status::Active $status): void {}
Also, I'm not really a fan of mixing literal types with unions.
function foo(int|'bar' $param): void {}
To me, mixing these makes it harder to reason about what a function
actually accepts. The whole point of literal types is to be (more)
precise but the moment you throw a wide type like int into the union,
that precision goes out the window. If a function takes int|'bar', what
does that really tell me? It feels like it defeats the purpose.
--
Regards,
Jordi Kroon
Hi Jordi,
Does the RFC allow referencing constants in type positions, or only raw
literal values?function foo(STATUS_ACTIVE|STATUS_INACTIVE $sort): void {}
Regarding your first point, referencing constants in type positions is not
supported. This RFC focuses specifically on scalar literals. A constant
access is not a literal itself, and allowing something like FOO in a type
position could cause ambiguity; at first sight, it's unclear whether it
refers to a class name or a constant. While casing might suggest one or the
other to a human, PHP allows both classes and constants to use any casing
style, which could lead to confusion.
This would also be a BC break, as the following is currently permitted in
PHP:
const Foo = 1;
class Foo {}
function bar(Foo $hello): void {}
If Foo were changed to mean 1 instead of an instance of Foo depending
on the surrounding context, this would break existing applications.
What about enum values? For example: function bar(Status::Active->value $status): void {} // or simply as function bar(Status::Active $status): void {}
For the second point, using enum values is also out of scope.
Status::Active->value is a runtime expression, not a literal: the engine
would have to confirm that Status is an enum, that the case exists, and
that it is backed before reading ->value. And Status::Active by itself
is an object, a single enum case, which would be a single-case type, a
separate feature from scalar literals. In both situations, requiring the
enum type itself is usually the better fit.
Also, I'm not really a fan of mixing literal types with unions.
function foo(int|'bar' $param): void {}
Finally, regarding mixing literal types with wider types like int|'bar',
this is supported by design. I believe restricting what may appear in a
union is the wrong approach; PHP should treat types uniformly, with
exceptions only for types that aren't value types and so are meaningless in
a union (void, never) or that are redundant (mixed|T, or a literal
already covered by its base type like int|1). If a union like
float|'cold'|'hot' lets a user express a real requirement for their API,
the type system should allow it.
Cheers,
Seifeddine.
It may be worth mentioning that within the Pattern Matching RFC future
scope, and mentioned a couple times within the discussion thread for the
RFC, there is a similar proposal that would allow for this without using
specific literal types.
It's outlined in
https://github.com/Crell/php-rfcs/blob/master/pattern-matching/future.md
under "Parameter or return guards"
It would allow for:
function setLogLevel (string $level is 'debug' | 'info' | 'warning' |
'error'): void {}
Of course, I don't think that a potential future scope of an in-draft RFC
is reason to dismiss a more direct implementation of literal scalar types,
but it may be useful to compare other ways we could achieve the same
functionality. I personally find pattern matching within parameter/return
types more versatile while keeping direct typing system more simplified.
Specifically for a range feature that Ben Ramsey brought up, pattern
matching for parameters seems much more appropriate.
All that being said, I would gladly welcome literal scalar types.
On Sun, Jun 14, 2026, 21:24 Seifeddine Gmati azjezz@carthage.software
wrote:
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
Den 15. jun. 2026 kl. 19.23 skrev Sarina Corrigan sarina.corrigan@gmail.com:
It may be worth mentioning that within the Pattern Matching RFC future scope, and mentioned a couple times within the discussion thread for the RFC, there is a similar proposal that would allow for this without using specific literal types.It's outlined in https://github.com/Crell/php-rfcs/blob/master/pattern-matching/future.md under "Parameter or return guards"
It would allow for:
function setLogLevel (string $level is 'debug' | 'info' | 'warning' | 'error'): void {}Of course, I don't think that a potential future scope of an in-draft RFC is reason to dismiss a more direct implementation of literal scalar types, but it may be useful to compare other ways we could achieve the same functionality. I personally find pattern matching within parameter/return types more versatile while keeping direct typing system more simplified. Specifically for a range feature that Ben Ramsey brought up, pattern matching for parameters seems much more appropriate.
All that being said, I would gladly welcome literal scalar types.
Hello Internals,
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
Thanks,
Seifeddine.
Hi,
This sounds very promising. But I would be confused about receiving a TypeError when just the value of the parameter is wrong.
As described in the RFC if I send 4 to a parameter that could only be 1, 2 or 3. I would expect a TypeError if I sent “abc” ?
Kind regards
Lars Nielsen
Hi
I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.
- RFC: https://wiki.php.net/rfc/literal_scalar_types
- Implementation: https://github.com/php/php-src/pull/22314
I have given the RFC a quick first review pass and would suggest to
leave out support for float literals. From a conceptual perspective
floats are much closer to being continuous values than they are to
discrete values and picking individual values from a continuous range is
typically not all that useful.
Support for floats is also going to invite the usual confusion about
implicit rounding:
function tenths(
0.0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9 $tenth
): void { var_dump($tenth); }
where tenths(0.1 + 0.2) will result in a TypeError. The RFC is also
unclear what values are valid floating point literals. As an example, is
4e3 a valid float literal? Is NAN a valid float literal?
Best regards
Tim Düsterhus