Protocol composition doesn't work with T even when <T: AnyObject>

Consider the following code:

protocol Foo: AnyObject {}
func doSomething<T>(factory: @escaping () -> (T & Foo)) {
    // ...
}

This fails to compile because the protocol composition T & Foo only works if T is either a class or another protocol. (Non-protocol, non-class type 'T' cannot be used within a protocol-constrained type)

However, this fails even when T is guaranteed to be a class:

func doSomething<T: AnyObject>(factory: @escaping () -> (T & Foo)) {
    // ...
}

Is this a limitation of the compiler or have I misunderstood what T: AnyObject would actually mean in this context?

3 Likes

Why not simply:

func doSomething<T: Foo>(factory: @escaping () -> T) { ... }

The question is not about how to make the snippet compile, the question is why it fails to compile. The snippet is just an example.

Got you. A simpler example:

protocol P {}
func foo<T: AnyObject>(_ v: T & P) {} // 🛑 
// Non-protocol, non-class type 'T' cannot be used within a protocol-constrained type

Looks like a compiler limitation that it can't figure out that T is a class type.

I’m not an expert so take this with a grain of salt but it looks like a generic type is not considered to be a protocol or class type so that’s why it can’t be used in a protocol-constrained type.

But you can make the generic type with the protocol constraints…

protocol P {}
func foo<T: AnyObject & P>(_ v: T) {} //✅

Swift doesn't support generic subclass constraints. You can't write class Foo<T>: T, P or func foo<T: AnyObject, SubT: T & P> either. At first blush, it doesn't seem like T & P as an existential type would be meaningfully different from moving the P constraint onto the generic parameter like trochoid suggested.

1 Like

I’ve definitely wanted this before and have had to fall back to runtime type checking for lack of this. Consider MultitypeService which has four public initializers. All four of them take a collection containing Any.Type, but it is a programming error (and fatalError* at runtime) for any of the types to not be a supertype of the class’s generic type argument.

*In certain code paths, the value is casted to the specified type(s) with as!, which would fail.