Abstract Type Members
See original GitHub issueSuggestion
TypeScript’s generics are very powerful but I occasionally run into problems with them leaking out into the rest of the code. One way to assist with this would be via the introduction of what Scala calls an ‘Abstract Type Member’. Perhaps other languages call them something else, I don’t know, it’s a concept I was introduced to via Scala.
In this Stack Overflow question a user gives an example of the concept in Scala:
abstract class Buffer {
type T
val element: T
}
and then its comparison with generics:
abstract class Buffer[T] {
val element: T
}
A discussion ensues, with various links to other places the concept is discussed. I will discuss my specific use case below and why I think it could be useful in TypeScript.
🔍 Search Terms
abstract type member
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn’t be a breaking change in existing TypeScript/JavaScript code
- This wouldn’t change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript’s Design Goals.
⭐ Suggestion
I’d like to be able to do this:
abstract class Parent {
abstract type AcceptableType
}
class Child extends Parent {
type AcceptableType = string
}
📃 Motivating Example
Whist rewriting jsPlumb in TypeScript I’ve been keen to further separate out the renderer from the core, with a view to supporting server side rendering and also rendering to a single SVG element in the browser (and perhaps other variations on these themes). To that end, I need to have some concept of the type of thing that actually represents a node. When rendering in a browser, that type is HTMLElement
, for example.
There is a connect
method on the core. For simplicity’s sake I’ll show a cut-down version of it that just focuses on the key arguments:
connect(source:HTMLElement, target:HTMLElement):Connection
We see that connect
can connect two HTMLElement
s together, as you’d expect. But what if the code was running server side, and the type of the connectable element was something else, let’s say HeadlessElement
. I’d want this method to read:
connect(source:HeadlessElement, target:HeadlessElement):Connection
So then in Core
I want the type of thing we’re connecting to be abstracted. With generics the approach would be:
abstract class Core<T> {
connect(source:T, target:T):Connection
}
class BrowserInstance extends Core<HTMLElement> {
}
Now we can pass HTMLElement
objects into connect
. But we’ve introduced a class-wide generic parameter to Core
, meaning any other part of the code that references an instance of Core
has to deal with generics. For instance there’s an Anchor
class, which is an abstraction of the concept of where on an element a connection is located, and it has a reference to an instance of Core
:
class Anchor {
constructor(private instance:Core<????>) { }
}
I’ve put ????
as the type parameter there because Anchor
couldn’t care less about it. To get this to compile I could of course simply discard the generic type:
class Anchor {
constructor(private instance:Core<any>) { }
}
but that’s messy and noisy. Anchor - and many other classes in the core - does not care that there is a specific case in which a generic type is required. The abstraction leaks out all over the place and requires updates to a large number of files, for no specific reason.
connect
with abstract type member
If I could do this:
abstract class Core {
abstract type T
}
class BrowserInstance extends Core {
type T = HTMLElement
}
Then I could write connect
as:
connect(source:T, target:T):Connection
and I wouldn’t have to leak that generic type out elsewhere into the code.
Alternative: parameterise the method
It would be possible to just parameterise the connect
method:
connect<T> (source:T, target:T):Connection {
...
}
but I don’t think that is a satisfactory solution for a couple of reasons:
- it requires the user supply the type parameter at the call site, which, although probably known, tends to go against the concept of type inference, and also introduces unnecessary noise
- it does not allow the type
T
to be shared with other methods. There could be other methods on the class that also need access to this abstraction.
💻 Use Cases
What do I want to use this for?
I want to neatly abstract out one specific piece of the data model that a subclass is required to provide.
What shortcomings exist with current approaches?
Generics provides only a very broad, high level solution, which introduces vast amounts of noise into the codebase.
What workarounds are you using in the meantime?
I’m not really using any workarounds; I find them distasteful and this is new code. I could just use any
:
connect(source:any, target:any):Connection
but who wants to do that?
I could support some intermediate interface and force subclasses to slot their types in behind it or do some casting:
connect(source:JsPlumbElement, target:JsPlumbElement):Connection
export interface BrowserJsPlumbElement extends jsPlumbElement, HTMLElement {}
but again this is just a trick to fool the compiler, whereas an abstract type member directly addresses the issue: What type of thing do you want to connect? I want to connect this type of thing.
My example here is just one example of countless that I’ve come across in the real world. They don’t come up all the time, but when they do, the abstract data type mechanism always delights me in its fitness for purpose.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:3
- Comments:13 (6 by maintainers)
@yume-chan Yesterday I updated jsPlumb to implement the scheme that was suggested to me in the first response to this issue, since for my use case it’s definitely better than not having anything at all. While doing so I came across a whole bunch of times that I wanted to be able to reference what would be the type member - and I wanted to do it by referring to the class, not by referring to an instance of the class.
I have a feeling in fact that this class vs instance question might be largely resolved by what’s actually possible within the code (without major surgery,) and I haven’t actually started to dig around very much yet. But yes, I think my preference also would be to access the type on a class, not on an instance of a class.
Ok. There seems to be a fair amount of subjectivity in that assessment. Squaring off “time to run our test suites” versus the usefulness of the feature to future users is not a piece of maths you could possibly do empirically, for instance. There must be a certain element of personal opinion in a judgement like that. Of course it’s informed opinion from someone who’s been close to the project for years so it’s bound to be of more utility than that of someone who has just arrived. I understand the sentiment in the linked post; people are always asking me to add various kitchen sinks to jsPlumb and I reject many of the suggestions for the same reasons discussed there. I wouldn’t have suggested this for Typescript if I didn’t genuinely think it would be of use.
Don’t take this the wrong way, but the generic based solution you suggested was like someone telling me I could open a tin can with a knife. It’s a good party trick, but messy, and shot through with ways to go wrong.
I intend to work on this on a fork anyway as it will be an interesting learning experience working through the code. And then I’ll also be able to get an answer, at least for myself, to the questions of how much bigger the compiler is, how much longer the tests take to run, etc. I won’t close this issue, though. That would be like giving up. You can close it if you like 😉