microsoft / TypeScript Public
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Abstract Type Members #42249
Comments
I don't see anything here you can't already do today, with the same ergonomics, with generics. For example, you could default the generic and bundle up any abstract members into a container type so there's only ever one type argument: class Base<T extends { a: unknown, b: unknown } = any> {
connect(arg1: T["a"], arg2: T["b"]) {
}
}
class Derived extends Base<{ a: string, b: number}> {
}
const m = new Derived();
m.connect("hello", 42);
// References to the generic type are unneeded
function f(b: Base) {
} The only thing that's missing here for 100% fidelity to your suggestion is local type aliases in the class body, which is a separate suggestion. |
Consider what happens if I change
This compiles too, though:
Defaulting the generics to |
If those are the only deficits relative to what's currently available, then this isn't meeting the value-vs-complexity bar. |
What does the "Too Complex" label refer to? Your estimation of the internal changes required to implement this? |
All forms of complexity: Implementation, cognitive load for people learning the language, overall code size, chances of constraining future development, performance, total code size of the compiler, time to run our test suites, odds that we interfere with future TC39 syntax, etc... See also https://gist.github.com/stuartd/a100fb315f2e5ca9ed17551d8b538a60 |
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 ;) |
I think experimentation is always encouraged, as long as the expectations are clear. It's not that our team hasn't changed our minds about different design suggestions, but we can't guarantee that we'll merge PRs even if the work's already done. |
Yes that's cool, I have no expectations that if I continue on with it the end result could get merged. It's an interesting problem for me to tinker with anyway. |
I came here searching for something similar to C++'s "member typedefs" (https://riptutorial.com/cplusplus/example/14397/member-types-and-aliases)
My use case: I have some very complex generic classes, and methods on them returns some other very complex generic types. As a simplified example, let's take a look at the built-in interface Iterator<T, TReturn = any, TNext = undefined> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
} If I already have a
Problems:
Difference from merging interface with namespace: Another similar feature we already have is merging an interface declaration with a namespace: interface Foo {
value: Foo.Bar;
}
namespace Foo {
type Bar = string;
}
type BarAlias = Foo.Bar; It "looks alike" However namespaces can't be generic, thus an advantage of this proposal is that we can declare "type members" within generic types, and those "type members" should be able to use all generic parameters on the containing type. interface Foo<T> {
type Bar = T; // temporal syntax
value: Bar; // temporal syntax
}
type BarAlias = Foo<string>.Bar; // temporal syntax
declare const foo: Foo<string>;
type BarAliasFromInstance = (typeof foo).Bar; // temporal syntax Some other advantages:
|
@yume-chan To answer your questions:
I would actually re-write my original suggestion to be this now:
the difference being that
There's no notion that this be limited to abstract only - the way you have described it, as "normal members" that can be overridden and also abstract, is how I would envisage it working in TypeScript. In @RyanCavanaugh's initial response I think he actually identified what this proposal is: it's simply to add "local type aliases in the class body". But I'd like them to be able to be overridden and to be abstract.
In Scala you can access a type member from an instance of some class, but not as a static member on that class. For example, given this class:
Type
Regarding the third code snippet:
I must admit that personally I don't see a lot of value in this, in either of the two examples involving a generic type on the class, since the specification of the type parameter has just been made; you might just as well in fact have spun it around and made the code more logical anyway:
Whether there is value in being able to assign a type from a setup that doesn't involve generics:
...is a question for the community at large. As I said earlier, Scala supports it, so perhaps there are use cases. I've not used it myself, but I'm just one guy. |
No, In TypeScript you must prepend another abstract class Foo {
fn(): void; // Function implementation is missing or not immediately following the declaration. (2391)
} abstract class Foo {
abstract fn(): void; // correct
}
That's interesting. In C++ you can only access member typedefs using the class itself, not instances. However they are still considered as "members" of the class, because they require type instantiating: #include <iostream>
using namespace std;
template <class T>
class Foo {
public:
typedef T Item; // <- a simple alias
};
int main()
{
Foo<string>::Item value = "foo"; // <- I don't need an instance
cout << value << endl;
return 0;
}
In TypeScript, current syntax to get a member's type also uses the class name itself: class Foo<T> {
value: T;
}
declare const value: Foo<string>["value"]; Requiring an instance to access its members' type can also limit its usage, for example what if I want to use a member type in class declaration: class A<T> {
value: T;
}
class B<T> {
value: A<T>["value"] // my value will always have same type as `A`'s
} How can I get a variable of
In my Member types can be very complex and should be considered as internal implementation detail of the class, so declare it by yourself is difficult to write, difficult to read, and not future-proof. |
Ah yes, confusing the two languages there. So what I had first was correct, as in, what I wanted to suggest:
|
@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. |
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:
and then its comparison with generics:
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.
abstract type member
My suggestion meets these guidelines:
I'd like to be able to do this:
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:We see that
connect
can connect twoHTMLElement
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 sayHeadlessElement
. I'd want this method to read:So then in
Core
I want the type of thing we're connecting to be abstracted. With generics the approach would be:Now we can pass
HTMLElement
objects intoconnect
. But we've introduced a class-wide generic parameter toCore
, meaning any other part of the code that references an instance ofCore
has to deal with generics. For instance there's anAnchor
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 ofCore
:I've put
????
as the type parameter there becauseAnchor
couldn't care less about it. To get this to compile I could of course simply discard the generic type: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 memberIf I could do this:
Then I could write
connect
as: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:but I don't think that is a satisfactory solution for a couple of reasons:
T
to be shared with other methods. There could be other methods on the class that also need access to this abstraction.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
: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:
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.
The text was updated successfully, but these errors were encountered: