question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Abstract Type Members

See original GitHub issue

Suggestion

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 HTMLElements 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:open
  • Created 3 years ago
  • Reactions:3
  • Comments:13 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
sporrittcommented, Jan 26, 2021

@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.

1reaction
sporrittcommented, Jan 9, 2021

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 😉

Read more comments on GitHub >

github_iconTop Results From Across the Web

Abstract Type Members | Tour of Scala
Here we have defined an abstract type T . It is used to describe the type of element . We can extend this...
Read more >
What is an Abstract Type Member in Scala? - Educative.io
If a member has an incomplete definition in the class, it is abstract. In Scala, we can declare abstract type members in classes,...
Read more >
Scala: Abstract types vs generics - Stack Overflow
Abstract type members provide a flexible way to abstract over concrete types of components. Abstract types can hide information about internals ...
Read more >
Scala | Abstract Type members - GeeksforGeeks
A member of a class or trait is said to be abstract if that particular member does not have a complete definition in...
Read more >
Type members are [almost] type parameters - Typelevel
This is the first of a series of articles on “Type Parameters and Type Members”. Type members like Member. class Blah { type...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found