[PHP-DEV] [RFC] [Discussion] Literal Scalar Types

Hello Internals,

I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.

- RFC: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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.

Thanks,
Seifeddine.

On Mon, 15 Jun 2026 at 03:03, David Gebler <davidgebler@gmail.com> 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: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

Thanks,
Seifeddine.

Hi David,

I agree enums are the better fit for a lot of cases. but not all.

1. describing APIs that already exist. `array_filter`'s `$mode` really
accepts `0|1|2`, but it's typed `int` because 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.
2. 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.
3. 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, 15 Jun 2026 at 03:03, David Gebler <davidgebler@gmail.com> 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.

Thanks,
Seifeddine.

Hi David,

I agree enums are the better fit for a lot of cases. but not all.

  1. describing APIs that already exist. array_filter’s $mode really
    accepts 0|1|2, but it’s typed int because 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.

  1. 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.

  1. 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:27 AM David Gebler <davidgebler@gmail.com> wrote:

On Mon, Jun 15, 2026 at 3:13 AM Seifeddine Gmati azjezz@carthage.software wrote:

On Mon, 15 Jun 2026 at 03:03, David Gebler <davidgebler@gmail.com> 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.

Thanks,
Seifeddine.

Hi David,

I agree enums are the better fit for a lot of cases. but not all.

  1. describing APIs that already exist. array_filter’s $mode really
    accepts 0|1|2, but it’s typed int because 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.

  1. 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”

  1. 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 6/14/26 20:22, Seifeddine Gmati wrote:

Hello Internals,

I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.

- RFC: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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

On 15.06.26 06:53, Ben Ramsey wrote:

On 6/14/26 20:22, Seifeddine Gmati wrote:

Hello Internals,

I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.

- RFC: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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 |
+---------------------------------------------------------------------+

On Mon, 15 Jun 2026 at 06:23, Andreas Heigl <andreas@heigl.org> wrote:

On 15.06.26 06:53, Ben Ramsey wrote:
> On 6/14/26 20:22, Seifeddine Gmati wrote:
>> Hello Internals,
>>
>> I'd like to start the discussion on a new RFC adding literal scalar
>> types to PHP.
>>
>> - RFC: PHP: rfc:literal_scalar_types
>> - Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub
>>
>> 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 |
+---------------------------------------------------------------------+

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

On 15/06/2026 3:22 am, Seifeddine Gmati wrote:

Hello Internals,

I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.

- RFC: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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.

Hi

On 6/15/26 03:22, Seifeddine Gmati wrote:

I'd like to start the discussion on a new RFC adding literal scalar
types to PHP.

- RFC: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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

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.

Thanks,
Seifeddine.

Hi

On 6/15/26 04:12, Seifeddine Gmati wrote:

1. describing APIs that already exist. `array_filter`'s `$mode` really
accepts `0|1|2`, but it's typed `int` because 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 (PHP: 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.

2. 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 PHP: 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.

3. 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

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.

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

On Mon, 15 Jun 2026 at 19:25, Lars Nielsen <lars@lfweb.dk> wrote:

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 php-rfcs/pattern-matching/future.md at master · Crell/php-rfcs · GitHub 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: PHP: rfc:literal_scalar_types
- Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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 19:43, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 6/15/26 03:22, Seifeddine Gmati wrote:
> I'd like to start the discussion on a new RFC adding literal scalar
> types to PHP.
>
> - RFC: PHP: rfc:literal_scalar_types
> - Implementation: [RFC]: Literal Scalar Types by azjezz · Pull Request #22314 · php/php-src · GitHub

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 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`.

On Mon, 15 Jun 2026 at 19:55, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 6/15/26 04:12, Seifeddine Gmati wrote:
> 1. describing APIs that already exist. `array_filter`'s `$mode` really
> accepts `0|1|2`, but it's typed `int` because 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
(PHP: 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.

> 2. 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
PHP: 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.

> 3. 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.

On Mon, Jun 15, 2026, at 2:45 PM, Seifeddine Gmati wrote:

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

On 6/15/26 21:45, Seifeddine Gmati wrote:

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.

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: `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`.

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

On Mon, 15 Jun 2026 at 21:32, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 6/15/26 21:45, Seifeddine Gmati wrote:
> 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.

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: `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`.

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,
and `4e3` is a valid float literal normalizing to `4000.0`.
- That `NAN` and `INF` are constants, not literals, and so are not
accepted, for the same reason a bare `FOO` is 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?

1. Add support for literal string types. (2/3)
2. Add support for literal integer types. (2/3)
3. Add support for literal float types. (2/3)
4. 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