Re: [RFC] [Discussion] Literal Scalar Types

From: Date: Mon, 15 Jun 2026 19:49:06 +0000
Subject: Re: [RFC] [Discussion] Literal Scalar Types
References: 1 2 3 4  Groups: php.internals 
Request: Send a blank email to internals+get-131300@lists.php.net to get a copy of this message
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‡ > > á;ýd > > 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.

« previous php.internals (#131300) next »