The problem has nothing to do with using Class over Tag, and all to do with matching against a case object (such as IntegerTag and StringTag) over matching against a mere value (such as TagOfInteger, ClassOfInteger and ClassOfString).
Let's try to compile 4 variants of your first example:
Version 1:
class Tag[T] case object IntegerTag extends Tag[Int] case object StringTag extends Tag[String] def defaultValue[T](typ: Tag[T]): T = typ match { case IntegerTag => 0 case StringTag => "" }
Version 2:
class Tag[T] case class IntegerTag() extends Tag[Int] case class StringTag() extends Tag[String] def defaultValue[T](typ: Tag[T]): T = typ match { case IntegerTag() => 0 case StringTag() => "" }
Version 3:
class Tag[T] class IntegerTag extends Tag[Int] class StringTag extends Tag[String] def defaultValue[T](typ: Tag[T]): T = typ match { case _: IntegerTag => 0 case _: StringTag => "" }
Version 4:
class Tag[T] val IntegerTag: Tag[Int] = new Tag[Int] val StringTag: Tag[String] = new Tag[String] def defaultValue[T](typ: Tag[T]): T = typ match { case IntegerTag => 0 // error: type mismatch case StringTag => "" // error: type mismatch }
If you try to compile them you'll see that version 1, 2 and 3 compile fine, while version 4 does not. The reason is that in version 1, 2 and 3, the pattern matching allows the compiler to know for sure which type is T:
In version 1 we do case IntegerTag =>. Because IntegerTag is a case object, we know for sure that there cannot be any instance that is equal to IntegerTag (except for IntegerTag itself). So if there is a match here, the runtime type of IntegerTag can only be IntegerTag, which extends Tag[Int]. Thus we can safely infer that T = Int.
In version 2 we do case IntegerTag() =>. Here IntegerTag is a case class, and as such we know that there can only bea match here if typ is an instance of IntegerTag, which extends Tag[Int]. Thus we can safely infer that T = Int.
In version 3 we do case _: IntegerTag =>. In other words, we explictly match against the IntegerTag type. So once again we know that typ is of type IntegerTag, which extends Tag[Int], and we can safely infer that T = Int.
Now, the problem with version 4 is that we have no guarantee about the runtime type of typ. This is because in this version we just do case IntegerTag =>, where IntegerTag is a val. In other words, there will be a match if and only if typ == IntegerTag. The problem is that the fact that typ is equal to IntegerTag (or in other words that typ.==(IntegerTag) returns true) tells us nothing about the runtime type of typ. Indeed, one can very well redefine equality in such a way that it can be equal to instance of unrelated classes (or simply be equal to instances of the same generic class but with different type arguments). By example, consider:
val StringTag: Tag[String] = new Tag[String] val IntegerTag: Tag[Int] = new Tag[Int] { override def equals( obj: Any ) = { (obj.asInstanceOf[AnyRef] eq this) || (obj.asInstanceOf[AnyRef] eq StringTag) } } println(StringTag == StringTag) // prints true println(StringTag == IntegerTag) // prints false println(IntegerTag == IntegerTag) // prints true println(IntegerTag == StringTag) // prints true
IntegerTag == StringTag returns true, which means that if we passed StringTag to method defaultValue, there would be a match with case IntegerTag =>, even though StringTagactuall is an instance of Tag[String] rather than of Tag[Int]. This shows that indeed the fact the there is a match for case IntegerTag => tells us nothing regarding the runtime type of typ. And so the compiler cannot assume anything about the exact type of typ: we only know from its declared static type that it is a Tag[T] but T is still unknown.