[Pitch] Narrowed Any

@Dmitriy_Ignatyev

Since @Pippin in #26 made a related argument from a different angle and the two coordinate.

a general Int | String union doesn't seem sufficiently justified to me. In many years of development, I've never truly needed something like Int | String – every such need has been perfectly addressable with generic enums like Either / OneOf, or with a dedicated enum for a specific API.

Two responses, one general, one specific.

General: SE-0413 explicitly punted on multiple-error throws "until we have a way to spell A | B". A v1 that ships only the typed-throws spelling still has to introduce the type-level construct A | B — there's no shortcut to "throws-only" that avoids the type-system addition, because SE-0413's punt is specifically about the type spelling. Once the type-system construct exists, restricting it syntactically to only the throws position is more compiler work, not less, because every position-check needs an "is this inside a throws clause?" gate that the prototype currently doesn't need.

Specific: the value-level uses that motivate it (in addition to typed throws):

  1. Codable round-trip across mixed-shape JSON — Itai Ferber's 2018 forum post is the canonical motivation. Today's ecosystem workaround is the wrapper-enum-of-cases pattern, which mangles per-library and forces every consumer through a .case(_) destructure.
  2. SwiftUI's _ConditionalContent<_ConditionalContent<A, B>, C> ladder, and the buildEither boilerplate that emits it. Replacing it with T | F (proposal § Result builders) retires the wrapper struct and inherits the proposal's depth-1 principle for free.
  3. Container extensions: extension Array where Element == Int | String { ... } is a method-set scoped to a closed leaf set. Today this is spelled by introducing a marker protocol and conforming each leaf to it (high ceremony, doesn't compose across libraries — each library's marker protocol is a different type).
  4. Per-witness conformance synthesis: Set<Int | String>, JSONEncoder().encode(v), Comparable.< over Int | Double, \(v) interpolation. All work in the prototype today, all driven by the type-level construct existing at value position. Dropping value-level positions means dropping these.

Each of these is its own use case. The specific question I'd ask back: of these four, is there one where you'd say "no, we don't need that even in 5 years"? If not, the path-of-least-resistance is to ship them together with one type-system addition rather than four staged proposals each adding a syntactic position.

Where I'd agree with you: the value-level uses individually are weaker than typed-throws. Typed-throws is the tip of the spear; the rest are coherent value-axis applications of the same closed-conformer-set primitive. If the forum's read is "typed-throws yes, the rest deserve a separate pitch each", that's a coherent scoping; the cost is repeating the type-system addition four more times, and the typed-throws-only v1 still has to land the type-system construct anyway.

Either / OneOf, or with a dedicated enum for a specific API

Worth flagging the implicit concession in your own framing: you're saying every such need has been "perfectly addressable" with Either / OneOf / dedicated enums, which already says the underlying need is real and recurring — you've reached for a wrapper every time. The question isn't whether the need exists; it's whether A | B is a better surface for it than the workarounds. The proposal isn't asking you to stop using Either, OneOf, or hand-rolled enums — they keep working unchanged. It's offering one more option that addresses the same need with a more general spelling, cross-library type identity, no per-arity rewrite, and synthesised conformance for the six standard-library protocols out of the box. Picking between A | B and a wrapper enum stays a per-API call the developer makes; this proposal just adds the option, with enough advantages to be worth having.

Worth being concrete about why those workarounds don't compose, since that's the gap A | B actually closes. A library exporting Either<NetworkError, DecodingError> is not the same type as another library exporting Either<NetworkError, DecodingError> if either library defined its own Either (each library's Either mangles per-module). Even when both libraries use the standard library's Either, two libraries' Either<E1, E2> and Either<E2, E1> (different leaf order) are distinct generic instantiations, so a consumer composing them needs an explicit conversion. A "dedicated enum for a specific API" is great for that API; it's exactly the per-API wrapper that doesn't compose across APIs.

1 Like