-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
static abstract
methods and properties
#34516
Comments
https://github.com/AlCalzone/node-zwave-js/blob/d2e29322d0392e02b7d8e2d7c8c430cb8fcaa113/src/lib/commandclass/CommandClass.ts#L363 and especially this comment: In the Z-Wave protocol most of the functionality is in specific command classes. The linked class serves as a common base class for them. It should be abstract but due to current limitations it is not. Therefore I have these dummy implementations that just say "override me in a derived class if necessary". I'd much rather have them defined as abstract methods, so derived classes have to be explicit if they need an implementation for the method or not. |
I haven't tested this but doesn't
actually crash on the line before the "Error" comment, because in |
Reading through these options, the thing I keep coming back to is that you'd solve the problem if you could guarantee that the class being passed as a function/generic argument is concrete. I don't think there's an existing constraint for that, is there? Like, if I could write
where ETA: of course if a keyword were added that means "and is not abstract" that would also be a good resolution. Right? |
No it does not because |
Great point, I honestly forgot that native ES6 classes don't have an |
That would likely fall under type-directed emit and therefore is a nonstarter. |
@thw0rted I think the best alternative for the interface FooStatic<T extends Foo> {
new(): T;
initialize(o: T): void
}
abstract class Foo {
static createInstance<T extends Foo>(this: FooStatic<T>) {
const a = new this();
this.initialize(a);
return a;
}
}
Foo.createInstance() // error Foo is abstract and does not implement initialize
class Bar extends Foo { }
Bar.createInstance() //error Bar does not implement initialize
abstract class Baz extends Foo { static initialize(o: Baz) { } }
Baz.createInstance() //error Baz is abstract
class Ok extends Foo { static initialize(o: Ok) { } }
Ok.createInstance() // finally ok While The version above does not allow access to any statics While unfortunately this does not throw errors on class declaration, it does guarantee that any function |
This is the first time I've heard explicit |
@thw0rted The explicit interface FooStatic<T extends Foo> {
new(): T;
initialize(o: T): void
}
abstract class Foo {
private x;
}
function createInstance<T extends Foo>(cls: FooStatic<T>) {
const a = new cls();
cls.initialize(a);
return a;
} |
Just popping in with a use case. I am writing a system that deals with dynamically loading modules at the moment, where the module provides a default export which is an instance of an abstract class Here's a code snippet to try to explain what I mean: interface ExtensionManifest {
identifier: string;
...
dependsOn?: string[];
}
abstract class Extension {
static abstract MANIFEST: ExtensionManifest;
}
class ExtensionA extends Extension {
static MANIFEST = {
identifier: "extension-a";
}
} // Ok, correctly implements static property
class ExtensionB extends Extension {
static MANIFEST = {
dependsOn: ["extension-a"];
}
} // Error, static property MANIFEST does not fully implement ExtensionManifest
class ExtensionC extends Extension {
} // Error, static property MANIFEST does not exist on ExtensionC |
Also @RyanCavanaugh, I may be misunderstanding things, but on the flaws with option 3 in your original post it appears |
No more progress on this issue? It's been 3 years already 😞 |
@eddiemf which of the five proposals listed in the OP do you think we should be progressing on, and why? |
@RyanCavanaugh After going more deeply into the whole thread I can understand why it's been 3 years already 😅 I don't really agree with any of the current possible solutions and I also can't think of something better. My use case was just to enforce the implementation of the method in the subclass, but I can see it goes way beyond that for various reasons. And by the looks of it the thread about allowing static methods on interfaces is also stuck 😔 Well, it serves as a bump at least |
Commenting for future update notifications. Would love this feature as well. Also looking to enforce implementation of the method in subclasses 😅 |
Likewise. I've been through these threads several times and see so many conflicting things. Is there a clear work-around for achieving the Serializable abstract class as previously described? Apologies if I've missed something... abstract class Serializable {
abstract serialize (): Object;
abstract static deserialize (Object): Serializable;
} EDIT: Solution I've gone with for the moment abstract class Serializable {
abstract serialize (): Object;
}
class A implements Serializable {
serialize(): Object { return obj as Object; };
static deserialize(obj: Object): A { return new A() };
}
function useDeserialize<T extends Serializable>(
obj: Object,
serializable: { deserialize(obj: Object) }: T
): T {
return serializable.deserialize(obj);
}
useDeserialize(A); |
Since I also just stumbled on this, here is my wishes from a user-perspective. I want to be able to
However, none of the above options provides this functionality in a safe way. So, here is an idea: why not make the checks a bit clever. In general, we expect an abstract class to be inherited and fully implemented, and especially abstract methods and non-abstract methods calling abstract methods only make sense in that context. So that's what the compiler should provide.
Here's an example with comments how that would look like in code: abstract class A {
// obviously abstract, so A.foo() does not exist!
abstract static foo(): void
// externally abstract on A, so A.bar() does not exist!
static bar(): void {
this.foo(); // works, because we expect `this` to be a child implementation
}
}
A.foo(); // ERROR: foo() does not exist on A (because it is directly abstract)
A.bar(); // ERROR: bar() does not exist on A (because it is indirectly abstract)
class B extends A {
// not abstract anymore, so B.foo() exists
static foo(): void {}
}
B.foo(); // WORKS
B.bar(); // WORKS, because B.foo() is not abstract |
If anybody is still following this: I just linked here from another issue. It looks like this really isn't going anywhere, and neither is #33892. Is there another way to constrain a generic type parameter to say "this can only be generic on types that implement a static method |
@thw0rted Yeah but you have to work around a bit. You can do something like const func = <T extends {new(): YourClass, func (whatyouwant): whatyouwant}> (instance: InstanceType<T>) => {} |
I don't think I'm getting it. Check out this Playground example. I think I have to pass separate instance-type and class-type generic parameters, since I'm not hard-coding "YourClass" as in your example. I have to grab the static |
That doesn't infer the constructor type properly, so |
@arantes555 it looks like @jcalz you're right, if I pass Is the thing I'm trying to describe some kind of uncommon pattern, or an antipattern, or something? Some of the "rough edges" in TS I've found lately are because I'm trying to describe something weird or inadvisable, but this seems pretty straightforward and it's hard to believe it simply can't be done. |
Here's a rough sketch of "quasi-abstract" based on @minecrawler's suggestion that you can use today: // Names for clarity
type A_Static = typeof A;
interface A_Static_Concrete extends A_Static {
bar(): void;
}
// N.B. classes need not be abstract for this pattern
class A {
static foo(this: A_Static_Concrete) {
// OK
this.bar();
}
}
// Concrete now
class B extends A {
static bar() {
}
}
// Error
A.foo();
// OK
B.foo(); |
Any update or thoughts on this? |
@Rigidity no, the problems with all the possible approaches outlined in the OP remain effectively unsolved |
If you know that every subclass of a class with an abstract static method will implement it, you can assume it exists. Being able to call
Am I missing something here? This seems possible to do. I'd be willing to look into implementing and unit testing this myself if that would help... |
OK, but how does that work in practice? Let's say you took something that could operate on a class with a static class Foo {
abstract static create(): Foo;
}
class Derived extends Foo {
static create() {
return new Derived();
}
}
function fn(x: typeof Foo) {
return x.create();
}
fn(Foo); Where's the type error in this program that crashes, and why?
|
Ryan, last year there were some comments proposing a As an aside: doesn't a class with |
I would say |
Dear god, don't make me write a singleton class as a workaround 😭😭😭 |
I think that a modified version of abstract class Base {
abstract static getName(): string
static logName() {
console.log(this.getName())
}
}
Base.getName() // Should error: Cannot call static method 'getName' on an abstract class
Base.logName() // Should error: Cannot call static method 'logName' on an abstract class This will ensure that the function should never cause a runtime error without knowing the contents of the function (e.g. the compiler only has a type definition file). However, this implementation is impossible to use in any helpful manner: abstract class Base {
abstract static getName(): string
static logName() {
console.log(this.getName())
}
}
class Subclass extends Base {
static getName(): string {
return 'Subclass'
}
static logName() {
console.log(this.getName())
}
}
function doMagic(baseSubclass: typeof Base) {
let name = baseSubclass.getName() // Should error: Cannot call static method 'getName' on an abstract class
let instance = new baseSubclass() // Already errors: Cannot create an instance of an abstract class.
// Do something here....
}
doMagic(Subclass)
doMagic(Base) The main problem here is the limitation of abstract class Base {
abstract static getName(): string
static logName() {
console.log(this.getName())
}
}
class Subclass extends Base {
static getName(): string {
return 'Subclass'
}
static logName() {
console.log(this.getName())
}
}
function doMagic(baseSubclass: Extends<typeof Base>) {
let name = baseSubclass.getName()
let instance = new baseSubclass()
// Do something here....
}
doMagic(Subclass)
doMagic(Base) // Should error: Base is an abstract class. Try entering one of its children Would this be a potential solution, or am I missing something? Problems and potential points for input
|
I have a suggestion of my own to reconcile it, going off option 5 by taking the type-driven part of option 4 and tweaking it to both eliminate the failure modes and the need for generics while ensuring they can still be fully manipulated by the type system and minimizing breakage: abstract properties. Here's how abstract properties would work:
To take the examples from OP: Click to expand
Pros:
Cons:
|
This comment was marked as spam.
This comment was marked as spam.
Interesting to note / might provide some helpful contrast - Dart is chatting about the same problem dart-lang/language#356 (comment) |
This comment was marked as spam.
This comment was marked as spam.
Comments to the tune of "I can't believe this isn't done yet" will be hidden; there are real problems outlined in the OP and other comments that need constructive feedback. |
Hello. I would like to provide quick answer for situations from op's question. Quick recall of the context:Let's say you wrote a trivial hierarchy abstract class A {
static abstract doSomething(): void;
}
class B extends A {
static doSomething() { }
} Because and want to do: abstract class A {
static abstract initialize(self: A): void;
static createInstance() {
const a = new this();
this.initialize(a);
return a;
}
} SolutionLet's define type Valid behavior would be to throw error that abstract class A {
static abstract initialize(self: A): void;
static createInstance() {
const a = new this();
this.initialize(a); // ERROR: initialize can't be called on type `typeof A`, initialize is abstract
return a;
}
} But a user may narrow abstract class A {
static abstract initialize(self: A): void;
static createInstance(this: InstantiableSubclasses<A>) {
const a = new this();
this.initialize(a); // VALID
return a;
}
}
A.createInstance() // ERROR: initialize could not be called: 'this' type mismatch This looks like we provide default implementation for method, but only allow it's usage in fully concrete context abstract class Complicated {
static abstract setup(): void;
static abstract print(): void;
static abstract ship(): void;
static abstract shutdown(): void;
}
// does not allow to pass Complicated itself
function fn(x: InstantiableSubclasses<Complicated>) {
// will work
x.setup();
x.print();
x.ship();
x.shutdown();
} |
Is there a status update on this? I've run into this issue when trying to replicate some of our back-end to the front-end, since abstract static methods are used there, I would like to duplicate this logic in a similar way using TS. A small sample of how I would want it to look: export default abstract class Category extends Period {
year: number;
number: number;
constructor(start: Date, stop: Date, year: number, number: number) {
super(start, stop);
this.year = year;
this.number = number;
}
abstract static periodsInYear(year: number): number;
abstract static nth(year: number, number: number): Category;
static forYear(year: number): Category[] {
return [...Array(this.periodsInYear(year)).keys()].map(number => this.nth(year, number));
}
previous(): Category {
if (this.number > 1) {
return Category.nth(this.year, this.number-1);
}
return Category.nth(this.year-1, Category.periodsInYear(this.year-1));
}
next(): Category {
if (this.number < Category.periodsInYear(this.year)) {
return Category.nth(this.year, this.number+1);
}
return this.constructor.nth(this.year+1, 1);
}
}; Where each concrete subclass of The discussion is pretty long though, and I've failed to understand the gist of the problem, and if and when this will be implemented. Edit: The approach suggested here (#34516 (comment)) satisfies my use case, but it somehow feels a bit hacky, and doesn't provide type checking in the child class to the extent that I want it: it doesn't signal that the "abstract static" methods aren't defined, for example. I had to use the "trick" from Kamil's answer to https://stackoverflow.com/questions/13955157/how-to-define-static-property-in-typescript-interface to actually make the type checker succeed. |
Ideally I'd like to access public abstract static variables with a public static function in the base class. Would this be possible if this issue gets resolved? |
bump |
@Distortedlogic Please do not "bump" issues if you do not have anything of substance to add, this only serves to annoy everyone who are subscribed to the issue (as GitHub sends emails for each and every comment made) and to make the discussion unreadable. @QuintenPeulingCodesharpNl the issue with being able to use |
Learning again why currently TS doesn't support a way to use `abstract static` unfortunately. microsoft/TypeScript#34516 Eeps!
Establishing the need for a type whose members can only be concrete classes: Click to openIt seems to me that the only reason that this is a problem is that abstract classes exist concretely. If you define an abstract class, you can refer to that abstract class as an entity:abstract class MyClass {}
function fn (X: typeof MyClass): MyClass {
return new X()
}
fn(MyClass) This is still desirable in a few ways, because it lets you access non-abstract static properties on the class, and changing this would break existing TypeScript code: abstract class MyClass {
static concreteMember = 'Hello World'
}
function fn (X: typeof MyClass): string {
return X.concreteMember
}
fn(MyClass) Basically, to allow abstract static members without any ugly workarounds, you need to treat it as non-existent, but to allow concrete static members, you need to treat it as a concrete object in and of itself. This brings me to an alternate analysis of the typing involved: why not consider what can be constructed as abstract class MyClass {}
class MySubClass extends MyClass {}
function fn (X: new() => MyClass): MyClass {
return new X()
} The following code doesn't compile because fn(MyClass) However, this code runs fine: fn(MySubClass) This by itself doesn't solve the issue, because abstract class MyClass {
static concreteMember = 'Hello World'
}
function fn (X: new() => MyClass): string {
return X.concreteMember
} At a glance, this is a bit odd. If const fakeConstructor: new() => MyClass = new Proxy(class {}, { construct () { return new MySubClass() } }) It seems proxies are standing in the way this time. But regardless of whether that's worth tackling, function fn (X: concrete typeof MyClass): string {
return X.concreteMember
} This is not the only option. Something like Establishing the need a three-way abstractness distinction between static methods: Click to openWith this, the core problem may be solved, but this causes a different issue:abstract class MyClass {
abstract static foo (): string
static bar (): string {
return this.foo()
}
} This should obviously cause an error. However, if the goal is to provide a default implementation that subclasses can inherit from, it's not necessarily a flawed idea. What you'd want this to do is create a property abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
} Here, abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
}
abstract class MySubClass extends MyClass {
abstract static baz (): string {
return this.bar()
}
} It might seem strange to allow an implementation on an abstract method, but I'm choosing this approach for a reason:
However, this is only my opinion. There might be good arguments for options like Showing examples of this system in action: Click to openHere are some examples of how this would interact with the type system.Two examples that show that an abstract class cannot be instantiated: abstract class MyClass {}
function fn (X: typeof MyClass): MyClass {
return new X() // Compiler error: Cannot create an instance of an abstract class.
}
fn(MyClass) abstract class MyClass {}
function fn (X: concrete typeof MyClass): MyClass {
return new X()
}
fn(MyClass) // Argument of type 'typeof MyClass' is not assignable to parameter of type 'concrete typeof MyClass'. An example that shows that a concrete class can be instantiated: abstract class MyClass {}
class MySubClass extends MyClass {}
function fn (X: concrete typeof MyClass): MyClass {
return new X()
}
fn(MySubClass) // Runs without errors Two examples that show that an abstract static method is inaccessible on the class on which it is declared: abstract class MyClass {
abstract static foo (): string
static bar (): string {
return this.foo() // Compiler error: Property 'foo' does not exist on type 'typeof MyClass'.
}
} abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
}
console.log(MyClass.bar()) // Compiler error: Property 'bar' does not exist on type 'typeof MyClass'. An example that shows that an abstract static method is not accessible in non-concrete subclasses: abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
}
abstract class MySubClass extends MyClass {
static baz (): string {
return this.bar() // Compiler error: Property 'bar' does not exist on type 'typeof MySubClass'.
}
} An example that shows how everything works out if this feature is used correctly: abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
}
class MySubClass extends MyClass {
static foo (): string {
return 'Hello World'
}
static baz (): string {
return this.bar()
}
}
console.log(MyClass.baz()) // Logs 'Hello World' to the console without errors. An example that shows that abstract static methods must be implemented in concrete subclasses if a default implementation was not provided: abstract class MyClass {
abstract static foo (): string
abstract static bar (): string {
return this.foo()
}
}
class MySubClass extends MyClass { // Compiler error: Non-abstract class 'MySubClass' does not implement all abstract members of 'MyClass'.
static baz (): string {
return this.bar()
}
} For that last case, perhaps a slightly different error message should be generated, though. For example: I think this solves all the issues in the OP? It at least provides a solution to whether abstract fields are allowed to be used in static methods. TL;DR:
The actual terms I've chosen to represent these language features don't matter much, what matters is the idea behind them, which I think is sound in both concept and usage (though I am open to being proven wrong). |
This is a continuation of #14600 which had two separate features proposed in the same issue (static members in interfaces and abstract static class members)
Search Terms
static abstract method property properties implement concrete
Suggestion
Currently, this code is illegal:
It should be legal to have
abstract static
(static abstract
?) members.Use Cases
(what are they?)
Unresolved Questions
What calls of abstract static methods are allowed?
Let's say you wrote a trivial hierarchy
For an expression
x.doSomething()
, what are validx
s?Option 1: All of them
Because
this
isn't generic instatic
members, we should simply allow all invocations of abstract static methods. Otherwise code like this would be illegal:However, this means that TypeScript would miss straight-up crashes:
A.doSomething()
, which seems like a fairly large design deficitOption 2: None of them
Allowing crashes is bad, so the rule should be that
static abstract
methods simply don't exist from a type system perspective except to the extent that they enforce concrete derived class constraints:This is unergonomic because it'd be impossible to write a function that dealt with an arbitrary complex constructor function without tedious rewriting:
We know this is a problem because people get tripped up by it constantly when they try to
new
an abstract class:https://www.reddit.com/r/typescript/comments/bcyt07/dynamically_creating_instance_of_subclass/
https://stackoverflow.com/questions/57402745/create-instance-inside-abstract-class-of-child-using-this
https://stackoverflow.com/questions/49809191/an-example-of-using-a-reference-to-an-abstract-type-in-typescript
https://stackoverflow.com/questions/53540944/t-extends-abstract-class-constructor
https://stackoverflow.com/questions/52358162/typescript-instance-of-an-abstract-class
https://stackoverflow.com/questions/53692161/dependency-injection-of-abstract-class-in-typescript
https://stackoverflow.com/questions/52358162/typescript-instance-of-an-abstract-class
For
abstract
constructor signatures, the recommended fix of using{ new(args): T }
is pretty good because a) you need to be explicit about what arguments you're actually going to provide anyway and b) there's almost always exactly one signature you care about, but forstatic abstract
methods/properties this is much more problematic because there could be any number of them.This also would make it impossible for concrete
static
methods to invokeabstract static
methods:On the one hand, this is good, because
A.createInstance()
definitely does crash. On the other hand, this literally the exact kind of code you want to write with abstract methods.One solution would be the existence of an
abstract static
method with a body, which would be allowed to invoke otherabstract static
methods but would be subject to invocation restrictions but not require a derived class implementation. This is also confusing because it would seem like this is just a "default implementation" that would still require overriding (that is the bare meaning ofabstract
, after all):An alternative would be to say that you can't call any
static
method on anabstract
class, even though that would ban trivially-OK code for seemingly no reason:static
methods from calling same-classabstract
methodsOption 3: Indirection is sufficient
Why not just split the baby and say that the direct form
A.doSomething()
is illegal, butexpr.doSomething()
whereexpr
is of typetypeof A
is OK as long asexpr
isn't exactlyA
.This creates the dread inconsistency that a trivial indirection is sufficient to defeat the type system and cause a crash:
It's also not entirely clear what "indirection" means. Technically if you write
then
foo
isn't exactly the declaration of SomeStaticAbstractClass itself - it's an alias. But there isn't really anything distinguishing that fromconst p = A
above.Option 4: Indirection, but with generics
Maybe a trivial indirection as described in Option 3 isn't "good enough" and we should require you to use a constrained generic instead:
This turns out to be a bad option because many subclasses don't actually meet their base class static constraints due to constructor function arity differences:
This isn't even code we want people to write -- a generic type parameter used in exactly one position is something we explicitly discourage because it doesn't "do anything".
Option 5: Something else?
Anyone able to square this circle?
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: