Late to the party, but I have more value to add to this question.
Generics are used when you want to maintain the same implementation while providing a different type definition to clients. Consider the following:
const rabbitPopulation = new Population<Rabbit>();
const mousePopulation = new Population<Mouse>();
rabbitPopulation.add(rabbit) // ✅ will pass
mousePopulation.add(rabbit) // ❌ will not compile
both rabbitPopulation
and mousePopulation
have have the same method implementations while differing only by type signatures. When you model your code this way, you are implicitly saying that all "populations" should behave and act similarly.
Meanwhile, inheritance is used to extend or overwrite the implementation altogether.
class RabbitPopulation extends Population {
increase() { /* increase rabbit population in a specific way */}
}
const rabbitPopulation = new RabbitPopulation();
class MousePopulation extends Population {
increase() { /* increase mouse population in a specific way */}
}
const mousePopulation = new MousePopulation();
As you can see, both rabbitPopulation
and mousePopulation
can now have their own implementations of similar methods. This is useful when want to capture a semantic idea across two objects (i.e both are populations) while allowing them to differ in behaviors (they have different methods on each).
You can use both together to express your modelling in a very rich way by making the base class a generic. Consider the following:
class MousePopulation extends Population<Mouse> {/**/}
This way you can give similar implementations to basic functionalities across all semantic ideas of a "population" while providing specific implementations for subtypes of a population (i.e. MousePopulation) in a type rich way. But most times when you do this, you almost always want the Population<T>
to be extended and might as well benefit from making your the Population
class an abstract
one.
abstract class Population<T> {}
class MousePopulation extends Population<Mouse> {}
I prefer this setup because you allow common semantic ideas to be captured in a base class while allowing specific functionalities to be extended by subclasses