Why can’t I pass a Protocol.Type to a generic T.Type parameter?

P.Type vs. P.Protocol

There are two kinds of protocol metatypes. For some protocol P, and a conforming type C:

  • A P.Protocol describes the type of a protocol itself (the only value it can hold is P.self).
  • A P.Type describes a concrete type that conforms to the protocol. It can hold a value of C.self, but not P.self because protocols don’t conform to themselves (although one exception to this rule is Any, as Any is the top type, so any metatype value can be typed as Any.Type; including Any.self).

The problem you’re facing is that for a given generic placeholder T, when T is some protocol P, T.Type is not P.Type – it is P.Protocol.

So if we jump back to your example:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
    print(serviceType)
}

let test: P.Type = C.self

// Cannot invoke 'printType' with an argument list of type '(serviceType: P.Type)'
printType(serviceType: test)

We cannot pass test as an argument to printType(serviceType:). Why? Because test is a P.Type; and there’s no substitution for T that makes the serviceType: parameter take a P.Type.

If we substitute in P for T, the parameter takes a P.Protocol:

printType(serviceType: P.self) // fine, P.self is of type P.Protocol, not P.Type

If we substitute in a concrete type for T, such as C, the parameter takes a C.Type:

printType(serviceType: C.self) // C.self is of type C.Type

Hacking around with protocol extensions

Okay, so we’ve learnt that if we can substitute in a concrete type for T, we can pass a C.Type to the function. Can we substitute in the dynamic type that the P.Type wraps? Unfortunately, this requires a language feature called opening existentials, which currently isn’t directly available to users.

However, Swift does implicitly open existentials when accessing members on a protocol-typed instance or metatype (i.e it digs out the runtime type and makes it accessible in the form of a generic placeholder). We can take advantage of this fact in a protocol extension:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
  print("T.self = \(T.self)")
  print("serviceType = \(serviceType)")
}

extension P {
  static func callPrintType/*<Self : P>*/(/*_ self: Self.Type*/) {
    printType(serviceType: self)
  }
}

let test: P.Type = C.self
test.callPrintType()
// T.self = C
// serviceType = C

There’s quite a bit of stuff going on here, so let’s unpack it a little bit:

  • The extension member callPrintType() on P has an implicit generic placeholder Self that’s constrained to P. The implicit self parameter is typed using this placeholder.

  • When calling callPrintType() on a P.Type, Swift implicitly digs out the dynamic type that the P.Type wraps (this is the opening of the existential), and uses it to satisfy the Self placeholder. It then passes this dynamic metatype to the implicit self parameter.

  • So, Self will be satisfied by C, which can then be forwarded onto printType‘s generic placeholder T.


Why is T.Type not P.Type when T == P?

You’ll notice how the above workaround works because we avoided substituting in P for the generic placeholder T. But why when substituting in a protocol type P for T, is T.Type not P.Type?

Well, consider:

func foo<T>(_: T.Type) {
    let t: T.Type = T.self
    print(t)
}

What if we substituted in P for T? If T.Type is P.Type, then what we’ve got is:

func foo(_: P.Type) {
    // Cannot convert value of type 'P.Protocol' to specified type 'P.Type'
    let p: P.Type = P.self
    print(p)
}

which is illegal; we cannot assign P.self to P.Type, as it’s of type P.Protocol, not P.Type.

So, the upshot is that if you want a function parameter that takes a metatype describing any concrete type that conforms to P (rather than just one specific concrete conforming type) – you just want a P.Type parameter, not generics. Generics don’t model heterogenous types, that’s what protocol types are for.

And that’s exactly what you have with printType(conformingClassType:):

func printType(conformingClassType: P.Type) {
    print(conformingClassType)
}

printType(conformingClassType: test) // okay

You can pass test to it because it has a parameter of type P.Type. But you’ll note this now means we cannot pass P.self to it, as it is not of type P.Type:

// Cannot convert value of type 'P.Protocol' to expected argument type 'P.Type'
printType(conformingClassType: P.self) 

Leave a Comment