It is perhaps first useful to distinguish between a type and a class and then dive into the difference between subtyping and subclassing.
For the rest of this answer I'm going to assume that the types in discussion are static types (since subtyping usually comes up in a static context).
I'm going to develop a toy pseudocode to help illustrate the difference between a type and a class because most languages conflate them at least in part (for good reason that I'll briefly touch on).
Let's start with a type. A type is a label for an expression in your code. This label's value and whether it is consistent (for some type system-specific definition of consistent) with all the other labels' value can be determined by an external program (a typechecker) without running your program. That's what makes these labels special and deserving of their own name.
In our toy language we might allow for the creation of labels like so.
declare type Int
declare type String
Then we might label various values as being of this type.
0 is of type Int
1 is of type Int
-1 is of type Int
...
"" is of type String
"a" is of type String
"b" is of type String
...
With these statements our typechecker can now reject statements such as
0 is of type String
if one of the requirements of our type system is that every expression has a unique type.
Let's leave aside for now how clunky this is and how you're going to have problems assigning an infinite number of expressions types. We can return to it later.
A class on the other hand is a collection of methods and fields that are grouped together (potentially with access modifiers such as private or public).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
An instance of this class gets the ability to either create or use preexisting definitions of these methods and fields.
We could choose to associate a class with a type such that every instance of a class is automatically labeled with that type.
associate StringClass with String
But not every type needs to have an associated class.
# Hmm... Doesn't look like there's a class for Int
It's also conceivable that in our toy language not every class has a type, especially if not all our expressions have types. It's a bit trickier (but not impossible) to imagine what type system consistency rules would look like if some expressions had types and some didn't.
Moreover in our toy language these associations do not have to be unique. We could associate two classes with the same type.
associate MyCustomStringClass with String
Now keep in mind there's no requirement for our typechecker to track the value of an expression (and in most cases it won't or is impossible to do so). All it knows are the labels you've told it. As a reminder previously the typechecker was only able to reject the statement 0 is of type String
because of our artificially created type rule that expressions must have unique types and we already had labeled the expression 0
something else. It didn't have any special knowledge of the value of 0
.
So what about subtyping? Well subtyping is a name for a common rule in typechecking that relaxes the other rules you might have. Namely if A is subtype of B
then everywhere your typechecker demands a label of B
, it will also accept an A
.
For example we might do the following for our numbers instead of what we had previously.
declare type NaturalNum
declare type Int
NaturalNum is subtype of Int
0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...
Subclassing is a shorthand for declaring a new class that allows you to reuse previously declared methods and fields.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
We don't have to associate instances of ExtendedStringClass
with String
like we did with StringClass
since, after all it's a whole new class, we just didn't have to write as much. This would allow us to give ExtendedStringClass
a type that is incompatible with String
from the typechecker's point of view.
Likewise we could have decided to make a whole new class NewClass
and done
associate NewClass with String
Now every instance of StringClass
can be substituted with NewClass
from the typechecker's point of view.
So in theory subtyping and subclassing are completely different things. But no language I know of that has types and classes actually does things this way. Let's start paring down our language and explain the rationale behind some of our decisions.
First off, even though in theory completely different classes could be given the same type or a class could be given the same type as values that are not instances of any class, this severely hampers the usefulness of the typechecker. The typechecker is effectively robbed of the ability to check whether the method or field you're calling within an expression actually exists on that value, which is probably a check you'd like if you're going to the trouble of playing along with a typechecker. After all, who knows what the value actually underneath that String
label is; it might be something that doesn't have, e.g., a concatenate
method at all!
Okay so let's stipulate that every class automatically generates a new type of the same name as that class and associate
s instances with that type. That lets us get rid of associate
as well as the different names between StringClass
and String
.
For the same reason, we probably want to automatically establish a subtype relationship between the types of two classes where one is a subclass of another. After all the subclass is guaranteed to have all the methods and fields the parent class does, but the opposite is not true. Therefore while the subclass can pass anytime you need a type of the parent class, the type of the parent class should be rejected if you need the type of the subclass.
If you combine this with the stipulation that all user defined values must be instances of a class, then you can have is subclass of
pull double duty and get rid of is subtype of
.
And this gets us to the characteristics that most of the popular statically typed OO languages share. There are a set of "primitive" types (e.g. int
, float
, etc.) which are not associated with any class and are not user-defined. Then you have all the user-defined classes which automatically have types of the same name and identify subclassing with subtyping.
The final note I'll make is around the clunkiness of declaring types separately from values. Most languages conflate the creation of the two, so that a type declaration also is a declaration for generating entirely new values that are automatically labeled with that type. For example, a class declaration typically both creates the type as well as a way of instantiating values of that type. This gets rid of some of the clunkiness and, in the presence of constructors, also lets you create label infinitely many values with a type in one stroke.