On Mon, 15 Jun 2026 at 19:55, Tim Düsterhus <[email protected]> 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
> (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.
>
> > 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
> 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.
>
> > 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.

Reply via email to