Description
Subclassing inheritance is an anti-pattern (see also this and this and this).
For those who aren't familiar with nominal typeclasses in for example Rust and Haskell, they conceptually (without getting into details and caveats yet) are a way of adding implementation to existing nominal types without modifying the source for those existing nominal types. For example, if you have an instance of existing nominal type A
and your want to invoke a function that expects a nominal type B
input, typeclasses conceptually enable you to declare the implementation of type B
for type A
and invoke the said function with the said instance of type A
as input. The compiler is smart enough to automatically provide (the properties dictionary data structure or in ECMAScript the prototype chain) to the function the type B
properties on the type A
instance.
The has efficiency, SPOT, and DNRY advantages over the alternative of manually wrapping the instance of A
in a new instance which has B
type and delegate to the properties of A
. Scala has implicit
conversion to automate, but this doesn't eliminate all the boilerplate and solve the efficiency (and tangentially note Scala also can implement a typeclass design pattern employing implicits). This disadvantage of wrapping with new instances compared to typeclasses is especially significant when the instance is a collection (or even collection of collections) of instances (potentially heterogeneous with a union type), all of which have to be individually wrapped. Whereas, with typeclasses only the implementations for each necessary pair of (target, instance) types need to be declared, regardless of how many instances of the instance type(s) are impacted.
And afaics this typeclass model is what the prototype
chain in EMCAScript provides.
When we construct a new instance, the A.prototype
of the constructor function A
is assigned to the instance's protoype
, thus providing the initial implementation for the instance (of type A
) of the properties of type A
. If we want to add an implementation of constructor function B
(i.e. of type B
) for all instances of type A
, then we can set A.prototype.prototype = B.prototype
. Obviously we'd like to type check that A
is already implemented for B
, so we don't attempt to implement B
for A
thus setting B.prototype.prototype = A.prototype
creating a cycle in the prototype chain.
That example was a bit stingy thus contrived, because actually we'd want a different implementation of type B
for each type we apply it to. And afaics this is exactly what typeclasses model.
I am very sleepy at the moment. When I awake, I will go into more detail on this proposal and try to justify its importance, relevance to TypeScript's goals, and important problems it solves.
Note I had recently discovered in my discussions on the Rust forum in May what I believe to be a complete solution to Wadler's Expression Problem of extensibility (the O in SOLID), which requires typeclasses and first class unions (disjunctions) and intersections (conjunctions).
There are 500+ detailed comments of mine (and @keean) over there (~335 of which are private) I need to reread and condense into what I want to say in this issue proposal. And it was to some extent and unfinished analysis that I had put on the back burner. I have elevated this priority seeing that TypeScript has the first-class unions and intersections and seeing that upcoming 2.1 is supposed to look into the nominal typing issue for #202.
I have mentioned typeclasses in a few of my recent comments on TypeScript issues.