It's absolutely a different approach to generics. Or, rather, that's the ringer. I want to say first: OCaml's take on modules is just a really nice way of doing namespacing as well.
Secondly, generics depend upon (a) having a means to discuss functionality which abstracts over one or more types and certain behaviors those types must support, (b) having a means to bundle up one or more types along with some behaviors, and (c) being able to combine those two.
In Typescript/Java/C# this is mostly carried out by classes and subtyping. Abstraction occurs when we ask not for a specific type but instead for something a little less than that specific type, one of its supertypes; bundling occurs in classes; and the combination occurs naturally as subtypes are transparently upcast to their supertypes.
There are two practical drawbacks to this approach:
First, it's hard to abstract over behavior that doesn't merely consume your abstract type but also returns it. When we do (c) via subclassing we have to upcast and it's not always clear or possible to re-downcast things back to the appropriate type. OO has tons of workarounds for this issue and related ones.
Second, it's hard to abstract over multiple interrelated types at once. For instance, a generic graph implementation might want to be abstract both in the types of nodes and the type of edges. The generic implementation can thus handle annotations at either the edges or the nodes. In OO abstraction, you might do something like have the edges be an associated type of the nodes, but this creates an unnecessary asymmetry.
The solution is a classic one. Instead of having the class represent an object, have the class represent a bundle of operations which act on abstract objects (the C++ vtable approach). For example, in pseudocode
class GRAPH
type Graph
type Node
type Edge
# These are hard to do with subclassing since Graph will often be upcast on return
def emptyGraph(): Graph
def simplify(g: Graph): Graph
# These represent non-trivial interactions between multiple types abstracted simultaneously
def addNode(g: Graph, n: Node): Graph
def neighbors(g: Graph, n: Node): List<Node>
And this, with the appropriate type discipline, is what OCaml does. Unfortunately, what you'll find is that OCaml's type discipline is critical and difficult to emulate. Making this sort of modularity work consistently involves some notions of equivalences and transparency that are natural to discuss when talking about modules but rarely show up in OO systems.
All the languages you mention have parameterized types, so I don't see why anyone would be tempted to use subtyping rather than generics. The only reason I could see is wanting to parameterize at runtime, but it's not immediately obvious to me that graphs with runtime parameterized edge and nodes are something you'd want on a regular basis. Am I missing some subtlety?
Parameterized types can help here a lot. I didn't want to speak to them too quickly so I blurred a few lines, but it's a good point.
Parametric types help with part (a) by allowing you to specify only part of the structure of your type. That can help enormously, though they also force some amount of concretion in your type which isn't always good. Ultimately, OCaml's module system is pointed in the direction of ad hoc polymorphism where you pass in behavior with your abstracted types.
Subtyping supports this passing of behavior as it lets you specify a whole space of types abstractly. In that way, it's a little more supportive of the pathway to ad hoc polymorphism.
Secondly, generics depend upon (a) having a means to discuss functionality which abstracts over one or more types and certain behaviors those types must support, (b) having a means to bundle up one or more types along with some behaviors, and (c) being able to combine those two.
In Typescript/Java/C# this is mostly carried out by classes and subtyping. Abstraction occurs when we ask not for a specific type but instead for something a little less than that specific type, one of its supertypes; bundling occurs in classes; and the combination occurs naturally as subtypes are transparently upcast to their supertypes.
There are two practical drawbacks to this approach:
First, it's hard to abstract over behavior that doesn't merely consume your abstract type but also returns it. When we do (c) via subclassing we have to upcast and it's not always clear or possible to re-downcast things back to the appropriate type. OO has tons of workarounds for this issue and related ones.
Second, it's hard to abstract over multiple interrelated types at once. For instance, a generic graph implementation might want to be abstract both in the types of nodes and the type of edges. The generic implementation can thus handle annotations at either the edges or the nodes. In OO abstraction, you might do something like have the edges be an associated type of the nodes, but this creates an unnecessary asymmetry.
The solution is a classic one. Instead of having the class represent an object, have the class represent a bundle of operations which act on abstract objects (the C++ vtable approach). For example, in pseudocode
And this, with the appropriate type discipline, is what OCaml does. Unfortunately, what you'll find is that OCaml's type discipline is critical and difficult to emulate. Making this sort of modularity work consistently involves some notions of equivalences and transparency that are natural to discuss when talking about modules but rarely show up in OO systems.