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.