Small question about type comparison

I believe what you're running into here is the difference between TypeA.Type and TypeA.Protocol. These are subtly different and unfortunately once generics are involved it is possible to write in code a type reference that looks like it should resolve to TypeA.Type when really it resolves to TypeA.Protocol.

The difference is a little bit hard to describe, but in short:

  • TypeA.Type refers to what is called the "existential metatype," which is the supertype of all types which conform to TypeA.
  • TypeA.Protocol refers to the "protocol metatype", which is the type of TypeA.self.

The (natural) language we have to describe all of this is... not great and I have talked myself in circles about it before. You can read this thread which contains what might be the most thorough exploration of .Type vs. .Protocol to date, though I'm not sure you'll come out of that thread less confused than going in. :smile:

It might be easiest to think about this in terms of examples. Because TypeA.Type is the supertype of all conforming types, there are things you can do with a value of type TypeA.Type that you can't do with a value of type TypeA.Protocol. For example, if TypeA has a static requirement, we can call that requirement on a value of type TypeA.Type:

protocol TypeA {
  static func test() -> Int
}

class TypeB: TypeA {
  static func test() -> Int { 0 }
}

func f (_ t: TypeA.Type) {
  print(t.test()) // 0
}

Because the underlying value of t must be some type which conforms to TypeA, we know that there must be an implementation of test() available for us to invoke. OTOH, the value TypeA.self has no such implementation because TypeA only specifies the requirement that conforming types must implement. So it is not possible to call test() on a value of type TypeA.Protocol.

The last piece of the puzzle here which I mentioned briefly above, is that in generic contexts, T.Type refers to T.Protocol when T is bound to an existential type, because we want it to be the case that T.self is always of type T.Type. (Notably, it is not the case that TypeA.self is TypeA.Type, but we sacrifice this in concrete contexts on the assumption that when people write TypeA.Type what they usually want is "some meta type which conforms to TypeA."

This extra wrinkle can result in some pretty confusing behavior, and I'm not actually certain whether the behavior you've identified here should be classified as a bug or not. We have all the concrete information needed to know that Optional<TypeA>.WrappedType is TypeA, so it's a little strange that the usual rule for generic meta type resolution would kick in. Maybe there's a reason for this, though.

5 Likes