Confused about singleton class

Can someone clarify a confusing point for me.

If I declare an object X extending class C, and I declare a case class Y extending class C, which classes and objects are defined.

Is there a class X of which the X object is an instance, and is there an object Y of class Y?

This is confusing because the List object is not of class List as far as I understand.

However, in the example above, as I understand the object X has type X and is guaranteed to be the only object with type X.

Also as I understand case class in the example above defines a companion object Y of the same name as the class. Does that object also have type Y?

I realized I don’t understand this when I tried to explain it to a non-Scala user.

A singleton object X, declared with the object keyword, has its own type X.type, of which it is the only instance. This type X.type is not related to a maybe already existing type X (so the type of a companion object is not related to the type of the related class).

So for your example, you have an object X, which is the only instance of X.type, and also an instance of C, as it is a superclass of X.

You have a type Y, which is a subtype of C, but no instances of it, until you create instances of your case class.

You further have the companion object Y, which is of type Y.type, which is different of the case class Y. This type also isn’t a subtype of C, as the companion object doesn’t extend anything by default.

You can verify these relations in the REPL:

@ class C;  object X extends C; case class Y(i:Int) extends C; val y = Y(1)
defined class C
defined object X
defined class Y
y: Y = Y(1)
@ X.isInstanceOf[C]
res1: Boolean = true

@ X.isInstanceOf[X.type]
res2: Boolean = true

@ Y.isInstanceOf[Y.type]
res3: Boolean = true

@ Y.isInstanceOf[Y]
res4: Boolean = false

@ Y.isInstanceOf[C]
res5: Boolean = false

@ y.isInstanceOf[Y.type]
res6: Boolean = false

@ y.isInstanceOf[Y]
res7: Boolean = true

@ y.isInstanceOf[C]
res8: Boolean = true

A key point (which took me a while to internalize) is that companion objects have a visibility relationship – they can see each other’s private members – but there isn’t automatically any inheritance relationship. You can declare an inheritance relationship explicitly, by making the companion object extend the class, but you have to spell it out – it’s actually a bit unusual (although not rare) to do so.

Case classes aren’t any different in this regard – they’re just special in that the companion object is auto-created, and filled with a couple of useful functions. But note that they are functions relating to the class, not on instances per se, which is why they go in the companion object…

Note that it’s a compiler error to define object X if class X is defined in a different file – companions have to be in the same file.

The something.type doesn’t only apply to singletons (objects). You can get a type of any value:

val x = "a"
val y: x.type = x // can't assign anything else

def method(input: AnyRef): input.type = {
   // this method must return 'input' as that's guaranteed by the signature
   input
}
val a = "b"
val b: a.type = method(a)

object ObjectName is a value too, that’s why you can write ObjectName.type to get its type.