Allow generic constraints for castable types (e.g., where A is B)

Motivation

Currently, Swift generics support only nominal constraints (T: U, T == U).
However, there are many practical cases where a type is castable but not subclassing or equal.
The compiler is already capable of determining these relationships at compile time, since the as operator performs a compile-time checked cast.
Still, there’s no way to express such “castability” in generic constraints.

A clear example is the Combine framework.
The subscribe method requires strict equality between a publisher’s Output and a subscriber’s Input, and also between their Failure types.
Yet in practice, these types are often safely castable, and the compiler already knows it:

// This could be valid in theory:
// - Output `Int` can be lifted to `Int?`
// - any `Failure` can be treated as `Error`
// - `Never` can be treated as any type.
AnyPublisher<Int, SomeError>.mock
    .subscribe(AnySubscriber<Int?, Error>.mock)

Currently, developers must add unnecessary bridging steps:

publisher
    .map { $0 as Int? }
    .mapError { $0 as Error }
    .subscribe(subscriber)

It’s possible to write additional subscribe methods for such cases, but that approach is verbose and leads to ambiguity.
Just to cover the examples above, one might need around 20 overloads (4 main cases plus ~16 ambiguous combinations).

These conversions are trivial, type-safe, and already known to the compiler, yet require explicit modifiers and additional overloads.

Also, this issue may become more relevant with the introduction of typed throws in Swift 6.

Proposed solution

Introduce a new form of castable generic constraint, for example:

func subscribe<T, F: Error>(_ subscriber: some Subscriber<T, F>) 
where T is Input, F is Failure {
    // ...
    receiveValue: { (input: T) in
      subscriber.receive(input as Input)
    } receiveError: { (error: F) in
      subscriber.receive(error as Failure)
    }
    // ...
}

This would mean:

“The compiler should accept this generic function if T can be safely cast to Input using as.”

This feature would also be useful when working with existentials - allowing a protocol type itself to satisfy a constraint that normally requires a concrete conforming type.

Currently, if you declare:

struct SomeStruct<Value: SomeProtocol> { ... }

you cannot instantiate it as:

SomeStruct<any SomeProtocol>()

because any SomeProtocol is an existential container, not a concrete type that conforms to SomeProtocol.

The only special exception in Swift today is Error, which can be used this way.

However, if Swift allowed a constraint like:

struct SomeStruct<Value> where Value is SomeProtocol { ... }

it would make this pattern valid, since the compiler already knows that any SomeProtocol can be cast to SomeProtocol.

This would remove the need for the current hardcoded exception for Error and generalize the behavior to any protocol.

It would make generic APIs more flexible and eliminate the need for existential-specific workarounds or wrapper types.

Paging @Slava_Pestov.

1 Like