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
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
adefaultcase 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
Fair points. I will happily concede that for the internal flag-style
APIs (rounding mode, array_filter, and so on) the enum migration
path you describe is a good fit, and the reuse you get from a named
symbol is a real benefit. I do not think literal types are the right
tool for everything enums cover.
On the === point specifically: enums compare with === to other
enum cases, but not to the scalar values they stand for.
Status::Success === 'success' is always false. So the moment your
data is actually a scalar, a string from json_decode, a value in an
associative array, a column from the database, the enum case is no
longer interchangeable with it; you have to map back and forth with
->value and ::from().
That is the case literal types are really aimed at, and it is clearest
with array shapes (which I have started working on and would put in
future scope). Consider typing a decoded JSON response:
public abstract function getResponse(): ['status' => 'success' |
'error', 'message' => null | string, 'data' => null | array, ...];
The values here are genuinely scalars on the wire. A status field
that is "success" or "error" is a discriminated union you can type
exactly, and it round-trips through json_encode / json_decode
untouched. This is everywhere in practice: tagged event payloads
({"type": "created" | "updated" | "deleted"}), result envelopes
({"ok": true, ...} vs {"ok": false, "error": string}), open/closed
flags, mode strings. Modelling these with enums means converting every
field on the way in and on the way out, even though the data never
stops being a plain string.
So I see them covering different ground: enums for named, reusable,
behaviour-carrying sets; literal types for describing scalar data that
already exists in its raw form, particularly structured payloads like
JSON.
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
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 Lars,
I think the confusion comes from treating "type" and "value" as two
separate things, which is the usual mental model. Literal scalar types
deliberately blur that line: each value is itself a type, a unit type
containing exactly one value.
So 1|2|3 is not "an int that happens to be restricted", it is the
union of three unit types 1, 2 and 3. Under that view, both 4
and "abc" fail for the same reason: neither is a member of the
declared type. There is no separate "the value is wrong" category, it
is all type membership, so a TypeError is the consistent outcome.
It is the same thing that already happens with the true type today:
passing false to a true parameter is a TypeError, even though
both are booleans.
On Mon, 15 Jun 2026 at 18:20, Sarina Corrigan sarina.corrigan@gmail.com
wrote:
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.
Hi Sarina,
Thanks for the pointer, that is a good read.
I don't think literal scalar types conflict with the pattern matching
future scope at all. The way I see it, a pattern is, or at least should be,
a type. $foo is Foo { x: 10 } is really asking "does $foo have the type
Foo with x equal to 10", and I would happily see us later allow Foo { x: 10 } as a type on its own. The one thing a pattern adds over a type
is binding: capturing a value in place of a sub-type, e.g.
if ($foo is Foo { x: $x }) { /* $x is bound here */ }
which a plain type declaration cannot do.
On the specific syntax in that document:
function setLogLevel(string $level is 'debug' | 'info' | 'warning' |
'error'): void {}
reads as redundant to me. The string contributes nothing once the value
set is given, so with literal scalar types the same intent is simply:
function setLogLevel('debug' | 'info' | 'warning' | 'error' $level): void {}
So I see the two as complementary rather than competing: literal types
provide the value-as-type building block, and pattern matching can build on
top of it for binding and destructuring.
Glad to hear you would welcome the feature.
Cheers,
Seifeddine
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
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
4e3a valid float literal? IsNANa valid float literal?Best regards
Tim Düsterhus
Hi Tim,
I think I might agree here, and others have raised the same concern on
Discord. Dropping float literals for now simplifies the RFC, so I am
inclined to do that, though I would like to hear what others think
before removing them.
For what it is worth, I am personally fine with tenths(0.1 + 0.2)
failing to match 0.3. This is not new behaviour: 0.1 + 0.2 == 0.3
is already false, and a match (0.1 + 0.2) already skips a 0.3 arm
for exactly the same reason. So a literal float type behaves
consistently with comparison and match, rather than introducing a
new surprise.
On your concrete questions: 4e3 is a valid float literal and works
today; it normalizes to 4000.0 (the type also stringifies as
4000.0). NAN and INF, on the other hand, are not literals but
constant identifiers that go through constant resolution. They are out
of scope for the same reason FOO is above: NAN $foo could just as
well mean a class named NAN.
Hi Tim,
I think I might agree here, and others have raised the same concern on
Discord. Dropping float literals for now simplifies the RFC, so I am
inclined to do that, though I would like to hear what others think
before removing them.
We debated allowing float as a backing type for enums, and eventually decided against it for a similar reason: It's not stable or discrete enough to be useful, and no other language we looked at supported them.
I'm still undecided on the RFC as a whole (I need to fully read it first), but would support limiting the literals to just int and string.
--Larry Garfield
Hi
For what it is worth, I am personally fine with
tenths(0.1 + 0.2)
failing to match0.3. This is not new behaviour:0.1 + 0.2 == 0.3
is already false, and amatch (0.1 + 0.2)already skips a0.3arm
for exactly the same reason. So a literal float type behaves
consistently with comparison andmatch, rather than introducing a
new surprise.
Yes, it is consistent with the existing behavior, but I don't think that
this makes it any less confusing. And for this specific proposal, adding
support for floats would be a deliberate decision rather than just
something that naturally follows from “existing === semantics” as with
match() which works on two values rather than values + types.
On your concrete questions:
4e3is a valid float literal and works
today; it normalizes to4000.0(the type also stringifies as
4000.0).NANandINF, on the other hand, are not literals but
constant identifiers that go through constant resolution. They are out
of scope for the same reasonFOOis above:NAN $foocould just as
well mean a class namedNAN.
Yes, I'm aware (based on a look at the implementation). This was
intended to be a subtle note that this is not explicitly spelled out in
the RFC.
The RFC text should comprehensively explain the behavior all possible
edge cases and ambiguities so that folks can form an educated opinion
based on the RFC text alone without needing to be able to understand the
implementation.
Writing that out, I also notice that the “Ecosystem” sub-section is
missing from the “RFC Impact” section (and the “To Existing Extensions
” sub-subsection would probably also be useful to know) and the voting
widget is also missing.
Best regards
Tim Düsterhus
Hi
For what it is worth, I am personally fine with
tenths(0.1 + 0.2)
failing to match0.3. This is not new behaviour:0.1 + 0.2 == 0.3
is already false, and amatch (0.1 + 0.2)already skips a0.3arm
for exactly the same reason. So a literal float type behaves
consistently with comparison andmatch, rather than introducing a
new surprise.Yes, it is consistent with the existing behavior, but I don't think that
this makes it any less confusing. And for this specific proposal, adding
support for floats would be a deliberate decision rather than just
something that naturally follows from “existing === semantics” as with
match()which works on two values rather than values + types.On your concrete questions:
4e3is a valid float literal and works
today; it normalizes to4000.0(the type also stringifies as
4000.0).NANandINF, on the other hand, are not literals but
constant identifiers that go through constant resolution. They are out
of scope for the same reasonFOOis above:NAN $foocould just as
well mean a class namedNAN.Yes, I'm aware (based on a look at the implementation). This was
intended to be a subtle note that this is not explicitly spelled out in
the RFC.The RFC text should comprehensively explain the behavior all possible
edge cases and ambiguities so that folks can form an educated opinion
based on the RFC text alone without needing to be able to understand the
implementation.Writing that out, I also notice that the “Ecosystem” sub-section is
missing from the “RFC Impact” section (and the “To Existing Extensions
” sub-subsection would probably also be useful to know) and the voting
widget is also missing.Best regards
Tim Düsterhus
Hi Tim,
That is fair. Float support should be a deliberate choice rather than
something that rides in on existing === semantics; the match
analogy only goes so far, since match compares two values whereas
this compares a value against a type.
You are also right that the RFC text has to stand on its own. I will
expand it to spell out the edge cases explicitly, including:
- Which numeric forms are accepted and how they normalize.
Hexadecimal, octal, binary and underscore-separated integer literals
(0x1A,0o17,0b101,1_000) all canonicalize to their value,
and4e3is a valid float literal normalizing to4000.0. - That
NANandINFare constants, not literals, and so are not
accepted, for the same reason a bareFOOis not. - String literal handling: single versus double quotes, escape
resolution, and the rejection of interpolation.
I will also add the missing "Ecosystem" and "To Existing Extensions"
subsections under "RFC Impact", and the voting widget.
On voting: rather than a single yes/no, would it make sense to split
it so each decision can stand on its own?
- Add support for literal string types. (2/3)
- Add support for literal integer types. (2/3)
- Add support for literal float types. (2/3)
- Coercion behaviour: coerce to the base type before checking
membership (as the RFC currently describes), or always require an
identity match. (1/2)
The last one is worth surfacing in particular. true, false and
null do not coerce at all today, even in coercive mode: passing 1
to a true parameter is a TypeError, not a coercion to true. If
we want literal scalars to be consistent with the existing value
types, an identity match is arguably the more natural behaviour, so I
would rather put it to the list than bake it in.
Best regards,
Seifeddine
Hey Seifeddine,
Am 15.06.2026 um 03:22 schrieb Seifeddine Gmati azjezz@carthage.software:
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 you are solving the wrong (or rather: only a specific subset of the) problem. What you actually probably want is pattern support in type positions. Let me know if I'm wrong in my assumption here.
I.e. basically support expressions as specified by https://wiki.php.net/rfc/pattern-matching in property and function argument/return positions. Which does this, and ranges and everything else you'd need.
Which is a worthwhile addition, but we should first get pattern-matching done, then we can do one RFC broadening the applicability of patterns.
Bob
Hey Seifeddine,
Am 15.06.2026 um 03:22 schrieb Seifeddine Gmati <azjezz@carthage.software
: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 you are solving the wrong (or rather: only a specific subset of
the) problem. What you actually probably want is pattern support in type
positions. Let me know if I'm wrong in my assumption here.I.e. basically support expressions as specified by
https://wiki.php.net/rfc/pattern-matching in property and function
argument/return positions. Which does this, and ranges and everything else
you'd need.Which is a worthwhile addition, but we should first get pattern-matching
done, then we can do one RFC broadening the applicability of patterns.Bob
Hi Bob,
My interest is not pattern matching; my focus is on the type system and
expanding it to be more expressive. My main motivation is that static
analysis tools (like PHPStan, Psalm, and Mago) already do many things that
PHP itself cannot, and I believe those features belong in the engine.
Literal types serve as a fundamental building block for future type system
features such as array shapes, tuples, and potentially even conditional
types.
While this RFC works alongside the pattern matching proposal by Larry and
Ilija, neither requires the other. The two are unrelated for several
reasons.
So to answer your question, no, I am not looking for pattern matching. I
see these as two distinct features that address different needs within the
language.
Cheers,
Seifeddine.
On Mon, Jun 15, 2026, 20:05 Seifeddine Gmati azjezz@carthage.software
wrote:
While this RFC works alongside the pattern matching proposal by Larry and
Ilija, neither requires the other. The two are unrelated for several
reasons.So to answer your question, no, I am not looking for pattern matching. I
see these as two distinct features that address different needs within the
language.
I don't believe they are addressing entirely different needs. I believe the
feature that static analysis tools support is pattern matching more than it
is typing. The question for me becomes whether patterns be represented
through types, or whether they should be matched against values.
Strictly denoting types and validating patterns are two separate concerns.
One asks "What can I do with this?" while the other asks "Is this within
expected bounds?". I am personally not against validating patterns within
type declarations, as is supported in Typescript, Scala (I believe), and to
an extent PHP with false|true. But I do believe they are different concerns.
I also think that, this being a form of pattern matching, if approved would
set a direction for the pattern matching RFC to treat patterns more closely
as types than assertions and guards (as their RFC currently proposes). I am
again not saying this is a bad thing, but it is worth acknowledging.
On Tue, Jun 16, 2026, 1:31 AM Sarina Corrigan sarina.corrigan@gmail.com
wrote:
On Mon, Jun 15, 2026, 20:05 Seifeddine Gmati azjezz@carthage.software
wrote:While this RFC works alongside the pattern matching proposal by Larry and
Ilija, neither requires the other. The two are unrelated for several
reasons.So to answer your question, no, I am not looking for pattern matching. I
see these as two distinct features that address different needs within the
language.I don't believe they are addressing entirely different needs. I believe
the feature that static analysis tools support is pattern matching more
than it is typing. The question for me becomes whether patterns be
represented through types, or whether they should be matched against values.Strictly denoting types and validating patterns are two separate concerns.
One asks "What can I do with this?" while the other asks "Is this within
expected bounds?". I am personally not against validating patterns within
type declarations, as is supported in Typescript, Scala (I believe), and to
an extent PHP with false|true. But I do believe they are different concerns.I also think that, this being a form of pattern matching, if approved
would set a direction for the pattern matching RFC to treat patterns more
closely as types than assertions and guards (as their RFC currently
proposes). I am again not saying this is a bad thing, but it is worth
acknowledging.
Hi Sarina,
I don't think we disagree here. I already said above that I think pattern
matching should match against types with variable binding; this actually
makes pattern matching easier because to expand it, you just have to expand
the type system, and patterns get expanded for free.
However, I think this is a discussion for the pattern matching RFC, not for
the literal types RFC.
Cheers,
Seifeddine.
While this RFC works alongside the pattern matching proposal by Larry and Ilija, neither requires the other. The two are unrelated for several reasons.
So to answer your question, no, I am not looking for pattern matching. I see these as two distinct features that address different needs within the language.
I don't believe they are addressing entirely different needs. I believe the feature that static analysis tools support is pattern matching more than it is typing. The question for me becomes whether patterns be represented through types, or whether they should be matched against values.
Strictly denoting types and validating patterns are two separate concerns. One asks "What can I do with this?" while the other asks "Is this within expected bounds?". I am personally not against validating patterns within type declarations, as is supported in Typescript, Scala (I believe), and to an extent PHP with false|true. But I do believe they are different concerns.
I also think that, this being a form of pattern matching, if approved would set a direction for the pattern matching RFC to treat patterns more closely as types than assertions and guards (as their RFC currently proposes). I am again not saying this is a bad thing, but it is worth acknowledging.
Hi Sarina,
I don't think we disagree here. I already said above that I think
pattern matching should match against types with variable binding; this
actually makes pattern matching easier because to expand it, you just
have to expand the type system, and patterns get expanded for free.However, I think this is a discussion for the pattern matching RFC, not
for the literal types RFC.Cheers,
Seifeddine.
This is interesting. We've approached patterns as a coincidental superset of the type system. That is, (almost) any type declaration is a valid pattern, but not because patterns are types; because we've implemented patterns to mirror types.
Patterns as types would be a completely different approach. I can see the appeal, but there's a number of issues there:
-
Performance. Patterns do a lot more work than a type check right now. If patterns cropped up in function signatures all over the place, that would have a notable, though currently unclear and hard to predict, impact on performance. (Far more inconsistent than, say, reified generics would have...)
-
Complexity. Expanding the type system to full patterns seems like it would be... hard. And possibly internal-API breaking. I could be completely wrong here, but it sounds like a fairly drastic change.
-
Repeatability. For this to work, I think it would have to include type definitions, which have always hit a discussion wall in the past as no one can agree on their design. Something like:
type positiveInt = int & >0;
type UserId = int;
type order = 'asc'|'desc';
function foo(positiveInt $val) { ... } // Guaranteed to be an integer greater than 0
function bar(UserId $id) { ... } // Would this accept a positiveInt? Debatable.
There's a sizable rabbit hole here. One could argue even that for scalar literal types we should have proper type defs.
- Variable binding. Let me be clear: Variable binding is the feature that makes pattern matching worthwhile. It's the reason we started working on it; we believe it is a prerequisite for properly implementing ADTs/"tagged enums." So even if patterns become types, there will still be a need for an extended variable binding syntax that works only inline, not as part of a type declaration. What the complexity impact of that would be, I have no idea.
If there is a consensus to go down this rabbit hole, I am not opposed to it. But it's a very deep rabbit hole, and exploring it would guarantee that neither patterns nor scalar literal types make it into this version. It would probably also entail 3-4 RFCs total, all of which would be kind of half-arsed on their own because they're part of a set; and PHP has been extremely, extremely bad at coordinating and working with that in the past. (Maybe a place for working groups?)
--Larry Garfield
Hey Larry,
Am 16.06.2026 um 17:16 schrieb Larry Garfield larry@garfieldtech.com:
While this RFC works alongside the pattern matching proposal by Larry and Ilija, neither requires the other. The two are unrelated for several reasons.
So to answer your question, no, I am not looking for pattern matching. I see these as two distinct features that address different needs within the language.
I don't believe they are addressing entirely different needs. I believe the feature that static analysis tools support is pattern matching more than it is typing. The question for me becomes whether patterns be represented through types, or whether they should be matched against values.
Strictly denoting types and validating patterns are two separate concerns. One asks "What can I do with this?" while the other asks "Is this within expected bounds?". I am personally not against validating patterns within type declarations, as is supported in Typescript, Scala (I believe), and to an extent PHP with false|true. But I do believe they are different concerns.
I also think that, this being a form of pattern matching, if approved would set a direction for the pattern matching RFC to treat patterns more closely as types than assertions and guards (as their RFC currently proposes). I am again not saying this is a bad thing, but it is worth acknowledging.
Hi Sarina,
I don't think we disagree here. I already said above that I think
pattern matching should match against types with variable binding; this
actually makes pattern matching easier because to expand it, you just
have to expand the type system, and patterns get expanded for free.However, I think this is a discussion for the pattern matching RFC, not
for the literal types RFC.Cheers,
Seifeddine.This is interesting. We've approached patterns as a coincidental superset of the type system. That is, (almost) any type declaration is a valid pattern, but not because patterns are types; because we've implemented patterns to mirror types.
Patterns as types would be a completely different approach. I can see the appeal, but there's a number of issues there:
- Performance. Patterns do a lot more work than a type check right now. If patterns cropped up in function signatures all over the place, that would have a notable, though currently unclear and hard to predict, impact on performance. (Far more inconsistent than, say, reified generics would have...)
The performance impact is certainly lower than manually validating all over the place.
Also, this would be a good motivation to invest into moving the type checks for known functions onto the caller side, and eliding them completely if the variable is unmodified. By now we have knowledge when variables can definitely not leak (are no references, no varvars or extract etc. are used) and check whether and what they are being assigned during the lifetime of the function.
In fact, using "int & < 10 & > 1" would give pretty strong bounds for opcaches data flow analysis to use too, giving much better hints at what integers will never be promoted to float, reducing the amount of guards necessary in JIT for example. An int which never can be < 0 for example saves a bounds check in one direction as well when working with packed arrays.
I.e. depending on the patterns and their integration into opcache quite a bit of potential could be unlocked.
But yes, you can always find worst case performances.
- Complexity. Expanding the type system to full patterns seems like it would be... hard. And possibly internal-API breaking. I could be completely wrong here, but it sounds like a fairly drastic change.
Why would it be particularly hard? We already have dedicated types APIs for stuff like type intersection and union. I don't think there would be a lot of impact.
- Repeatability. For this to work, I think it would have to include type definitions, which have always hit a discussion wall in the past as no one can agree on their design. Something like:
type positiveInt = int & >0;
type UserId = int;
type order = 'asc'|'desc';function foo(positiveInt $val) { ... } // Guaranteed to be an integer greater than 0
function bar(UserId $id) { ... } // Would this accept a positiveInt? Debatable.
There's a sizable rabbit hole here. One could argue even that for scalar literal types we should have proper type defs.
From the perspective of an autoloader, a type name is just like any other class name.
This is more composers problem to solve (they possibly could include a per-namespace fallback to a default.php or something, where you'd define all types of a namespace in).
We should have type names eventually, but I consider them separate from the basic pattern feature. Should be done, but separate RFC with its own concerns, and I also suppose a pretty simple RFC actually!
- Variable binding. Let me be clear: Variable binding is the feature that makes pattern matching worthwhile. It's the reason we started working on it; we believe it is a prerequisite for properly implementing ADTs/"tagged enums." So even if patterns become types, there will still be a need for an extended variable binding syntax that works only inline, not as part of a type declaration. What the complexity impact of that would be, I have no idea.
I don't think we would need to include binding in a first version of this.
If there is a consensus to go down this rabbit hole, I am not opposed to it. But it's a very deep rabbit hole, and exploring it would guarantee that neither patterns nor scalar literal types make it into this version. It would probably also entail 3-4 RFCs total, all of which would be kind of half-arsed on their own because they're part of a set; and PHP has been extremely, extremely bad at coordinating and working with that in the past. (Maybe a place for working groups?)
I also don't think that pattern matching needs particular changes to fit this. The syntax of pattern matching is pretty fine and could be just 1:1 translated to function args.
--Larry Garfield
Thanks,
Bob
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 all,
Based on the discussion so far, I've updated the Literal Scalar Types
RFC to v0.2:
https://wiki.php.net/rfc/literal_scalar_types
What changed:
- Strict matching is now the proposed default instead of coercion. A
literal type would never coerce, in either typing mode, exactly how
true, false and null already behave: passing 1 where the type is true
is a TypeError even with strict_types disabled. The goal is a single
rule: a type whose identity is one value matches only that value. - The vote is split into three: a 2/3 vote to add int and string
literals, a separate 2/3 vote to add float literals, and a
simple-majority vote for the matching semantics (strict vs coercive). - Float support is now its own, optional question. The RFC discusses
the precision issue in full (0.3 won't match 0.1 + 0.2, because 0.1 +
0.2 === 0.3 is already false today) along with the arguments for and
against, mirroring why floats were left out of enum backing types. - Documented the accepted literal syntax (0x.., 0b.., octal, 4e3,
1_000, and so on) and why named constants,INFandNANare excluded. - Added RFC Impact notes for extensions and tooling (parsers, IDEs,
formatters, linters, static analysers), plus a Future Scope section
covering array shapes/tuples and integer range types.
Thanks,
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.
Hello Interansl,
I'm planning to take this RFC to a vote next week. I just wanted to
check in since there hasn't been any discussion after the last update.
Any concerns regarding this?
Cheers,
Seifeddine.