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)
static abstract method property properties implement concrete
Currently, this code is illegal:
abstract class A {
static abstract doSomething(): void;
}
// Should be OK
class B extends A {
static doSomething() { }
}
// Should be an error; non-abstract class failed to implement abstract member
class C extends A {
}
It should be legal to have abstract static
(static abstract
?) members.
(what are they?)
What calls of abstract static methods are allowed?
Let's say you wrote a trivial hierarchy
abstract class A {
static abstract doSomething(): void;
}
class B extends A {
static doSomething() { }
}
For an expression x.doSomething()
, what are valid x
s?
Because this
isn't generic in static
members, we should simply allow all invocations of abstract static methods. Otherwise code like this would be illegal:
abstract class A {
static abstract initialize(self: A): void;
static createInstance() {
const a = new this();
this.initialize(a);
return a;
}
}
However, this means that TypeScript would miss straight-up crashes:
// Exception: 'this.initialize' is not a function
A.createInstance();
A.doSomething()
, which seems like a fairly large design deficitAllowing 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:
abstract class A {
static abstract doSomething(): void;
}
class B extends A {
static doSomething() { }
}
// Error, can't call abstract method
A.doSomething();
// This call would work, but it'd still be an error
const Actor: typeof A = B;
Actor.doSomething();
function indirect(a: { doSomething(): void }) {
a.doSomething();
}
// Error, can't use abstract method 'doSomething' to satisfy concrete property
indirect(A);
// OK
indirect(B);
This is unergonomic because it'd be impossible to write a function that dealt with an arbitrary complex constructor function without tedious rewriting:
abstract class Complicated {
static abstract setup(): void;
static abstract print(): void;
static abstract ship(): void;
static abstract shutdown(): void;
}
function fn(x: typeof Complicated) {
// Error, can't call abstract method
x.setup();
// Error, can't call abstract method
x.print();
// Error, can't call abstract method
x.ship();
// Error, can't call abstract method
x.shutdown();
}
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 for static 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 invoke abstract static
methods:
abstract class A {
static abstract initialize(self: A): void;
static createInstance() {
const a = new this();
// Error
this.initialize(a);
return a;
}
}
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 other abstract 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 of abstract
, after all):
abstract class A {
abstract static initialize() {
console.log("Super class init done; now do yours");
}
}
// No error for failing to provide `static initialize() {`, WAT?
class B extends A { }
An alternative would be to say that you can't call any static
method on an abstract
class, even though that would ban trivially-OK code for seemingly no reason:
abstract class A {
static foo() { console.log("Everything is fine"); }
}
// Can't invoke, WAT?
A.foo();
static
methods from calling same-class abstract
methodsWhy not just split the baby and say that the direct form A.doSomething()
is illegal, but expr.doSomething()
where expr
is of type typeof A
is OK as long as expr
isn't exactly A
.
This creates the dread inconsistency that a trivial indirection is sufficient to defeat the type system and cause a crash:
// Error; crash prevented!
A.doSomething();
const p = A;
// OK, crashes, WAT?
p.doSomething();
It's also not entirely clear what "indirection" means. Technically if you write
import { SomeStaticAbstractClass as foo } from "./otherModule";
foo.someAbstractMethod();
then foo
isn't exactly the declaration of SomeStaticAbstractClass itself - it's an alias. But there isn't really anything distinguishing that from const p = A
above.
Maybe a trivial indirection as described in Option 3 isn't "good enough" and we should require you to use a constrained generic instead:
// Seems like you're maybe OK
function fn<T extends typeof A>(x: T) {
x.doSomething();
}
// Good, OK
fn(B);
// A fulfills typeof A, fair enough, crashes, WAT?
fn(A);
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:
abstract class A {
constructor() { }
foo() { }
}
class B extends A {
constructor(n: number) {
super();
}
bar() { }
}
function fn<T extends typeof A>(ctor: T) {
// Want to use static methods of 'ctor' here
}
// Error, B's constructor has too many args
fn(B);
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".
Anyone able to square this circle?
My suggestion meets these guidelines:
Use Cases
(what are they?)
https://github.com/AlCalzone/node-zwave-js/blob/d2e29322d0392e02b7d8e2d7c8c430cb8fcaa113/src/lib/commandclass/CommandClass.ts#L363 and especially this comment:
https://github.com/AlCalzone/node-zwave-js/blob/d2e29322d0392e02b7d8e2d7c8c430cb8fcaa113/src/lib/commandclass/CommandClass.ts#L368
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
static createInstance() {
const a = new this();
// Error
this.initialize(a);
return a;
}
actually crash on the line before the "Error" comment, because in A.createInstance()
, this
is typeof A
, and you can't call new
on an abstract class?
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
function fn<T extends Concrete<typeof A>>(x: T) {
x.doSomething();
}
where Concrete
uses conditional type math to be never
if the generic argument is abstract. I don't quite have it figured out myself but it feels like something @dragomirtitian could come up with 馃
ETA: of course if a keyword were added that means "and is not abstract" that would also be a good resolution. Right?
[...] actually crash on the line before the "Error" comment
No it does not because abstract
has no effect at runtime--it's purely compile-time info. The class is perfectly new
able at runtime as the abstractness is only enforced by the compiler.
Great point, I honestly forgot that native ES6 classes don't have an abstract
keyword. It looks like the workaround would be to check new.target
in the abstract class's constructor, or see if nominally-abstract methods actually exist, and throw explicitly, but that would be a discussion for another issue.
That would likely fall under type-directed emit and therefore is a nonstarter.
@thw0rted I think the best alternative for the createInstance
and initialize
case is just to be upfront about the requirements of the class on the createInstance
function. We can explicitly type this
and make sure the passed in class is a concrete class derived from A
(ie has a callable constructor, that returns A
) and has any extra needed static methods:
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 initialize
is not an abstract static member of Foo
, it acts like one for any client that calls createInstance
. This makes a clear choice regarding the question 'What calls of abstract static methods are allowed?'. Foo.initialize
is not allowed as Foo
does not really have the static method. Any method in Foo
that requires access to initialize
must be explicit about this and have an annotation for this
.
The version above does not allow access to any statics Foo
defined, but this can be easily remedied with an intersection (ex)
While unfortunately this does not throw errors on class declaration, it does guarantee that any function Foo
that requires extra static methods is not callable on any defined class that does not define them. I personally think this is close enough, but results may vary 馃槉.
This is the first time I've heard explicit this
typing suggested as a resolution for the issue, and I think it does have a lot going for it. I maintain that it would be "nice" to have some declaration inside abstract class Foo { }
that tells implementing classes that they must implement a static initialize()
, but it seems like keeping the instance and constructor halves of a class in the same block is an argument I already lost. (That's the other "child issue" of #14600, BTW.)
@thw0rted The explicit this
typing is just because we want to use the 'abstract' statics inside the abstract class. If createInstance
were a regular function taking a class as a parameter, the types would look the same, just be applied to a regular parameter:
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 Extension
. I would like to have an abstract static property on Extension
which defines metadata about the extension, but this would differ for each extension, so I want to require it but not implement it on the abstract class. The reason I want this to be abstract, is so that I can check several key properties in the metadata before instantiating the class in any form (mostly for dependency resolution).
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 typeof p
returns typeof A
(looking at VSCode intellisense), where as if you have a class that correctly extends A (C for the purposes of this example), typeof C
returns C
, so is it not possible to discern the indirection (as typeof p
resolves to typeof A
anyways, which would be disallowed in this model)? I may be completely wrong here as I am not very familiar with the internals of the TypeScript engine, this just seems to be the case from experimenting within VSCode. This doesn't address the issue regarding import aliases with this model that you raised however.
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
abstract static
methodsHowever, 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 f(number, boolean)
"?
@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 serialize
implementation from val.constructor
but I can't guarantee that that exists. And the negative test at the bottom, trying to wrap an instance of a class with no serialize
method, doesn't actually fail. I must be missing something.
@thw0rted check this
That doesn't infer the constructor type properly, so new Wrapper(new Bar())
does not error when it should. Unless #3841 is addressed there's not much chance of getting the behavior you want without a type assertion or manually annotating your classes as having a strongly-typed constructor
property.
@arantes555 it looks like InstanceType<WrappedTypeConstructor>
just winds up being any
-- look at the return type of the call to getValue()
on L26 in your example.
@jcalz you're right, if I pass typeof Foo
explicitly, it works, and if I pass typeof Bar
explicitly it fails. I want the constructor argument to get flagged as invalid, though, and it sounds like that isn't going to happen.
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();
Building on @minecrawler's suggestion with an Implemented<T>
utility type:
abstract class A {
// obviously abstract, so A.foo() does not exist!
abstract static foo(): void
// This method explicitly specifies that `this` must be
// implemented and won't work on an abstract class:
static someFunc(this: Implemented<typeof A>) {
this.foo() // Works because the this in this function is implemented
}
// Later, if we did:
// `A.someFunc()`
// We would get the error that .someFunc()
// cannot be called on an abstract class.
// If we did something like this:
static bar(): void {
this.foo();
}
// Should Typescript error here? It could either infer
// that `this` should extend Implemented<typeof A> and only
// error if we explicitly set another `this` param in our function
// declaration that is incompatible, or Typescript could yell
// at us to explicitly set our `this` parameter.
}
class B extends A {
// not abstract anymore, so B.foo() exists
static foo(): void {}
}
// `B` satisfies `Implemented<typeof A>`, so B.someFunc() works fine.
Regarding the following snippet on an abstract class where .foo()
is abstract:
static bar(): void {
// `.foo()` is abstract!
this.foo();
}
Should this error? Here is what an explicit declaration could look like:
static bar(this: Implemented<typeof A>) {
this.foo();
}
Or cleaned up a little bit through an implemented
modifier, maybe?:
implemented static bar() {
this.foo();
}
Thoughts?
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.
1. Obviously, if there is an abstract method, it should not exist on the object from the outside. 2. Any non-abstract method which calls an abstract method, should be treated as abstract on the outside.
@minecrawler There's a problem with your approach though. How to deal with .d.ts
definition files?
There're no "bodies" in type definitions, that means the compiler had no ways to know if a method will call an abstract method.
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();
@RyanCavanaugh This still method still not working for protected static
members though.
Building on @minecrawler's suggestion with an
Implemented<T>
utility type:abstract class A { // obviously abstract, so A.foo() does not exist! abstract static foo(): void // This method explicitly specifies that `this` must be // implemented and won't work on an abstract class: static someFunc<T extends A>(this: Implemented<T>) { this.foo() // Works because the this in this function is implemented } ... } ...
@SploxFox Shouldn't that be Implemented<typeof A>
instead?
@JasonHK Oops. Fixed it
Here's a related scenario having to do with new
ing a concrete implementation of an abstract class.
abstract class AbstractType {}
class Foo extends AbstractType{}
class Bar extends AbstractType{}
function create(condition: 'foo' | 'bar'): AbstractType {
const ConstructorMap: Record<string, typeof AbstractType> = {
foo: Foo,
bar: Bar
}
return new ConstructorMap[condition]; // ERROR Cannot create an instance of an abstract class.
}
The workarounds are slightly annoying when the constructor has several parameters.
In terms of .d.ts
files, I'd expect them to work in much the same manner that inferred return types do: implicit in the .ts
file becomes explicit in the .d.ts
file:
function a() {
return 1;
}
abstract class Alpha {
abstract static initialize(self: Alpha): number;
static b(): Alpha {
const instance = new this();
this.initialize(instance);
return instance;
}
}
would compile to:
declare function a(): number;
declare abstract class Alpha {
abstract static initialize(self: Alpha): number;
static b(this: Implemented<typeof Alpha>): Alpha;
}
Essentially, we infer the type of this
based on the usage of the abstract initialize
call (and possibly the usage of new
). If this
is explicitly annotated to something else by the user, we should still be safe because we shouldn't make initialize
available on typeof Alpha
, and only on Implemented<typeof Alpha>
.
I agree that a built-in Implemented<T>
would be helpful in many aspects of abstract classes in general, even unrelated to abstract static methods, such as dinofx's example of wanting to pick a concrete implementation of an abstract class and use it at runtime.
@RyanCavanaugh I see a possible option 5 where we could mimic the behavior of the existing abstract class
. The idea is to add static abstract class
and static class
to the language. It's a special kind of class that would only allow static
members to exist - effectively boxing unsafe operations away.
static class
static
members, can be used anytime.static abstract class
static abstract
and static
members, must be extended/implemented before use. Therefore static abstract class
works similarly to abstract class
.
One inconvenient is that we would be forced to separate static abstract
members into a special static abstract class
. Not sure if it's really an inconvenient since static
and instance
layers are separated anyway. But it would be tidy, that's for sure.
static abstract class AS {
static abstract doSomething(): void;
static createInstance() {
const a = new this();
this.initialize(a); // this should only be allowed through casting
return a; // or rather make `createInstance` abstract as well
}
}
class A extends AS {
static doSomething() {
console.log('hello')
}
}
AS.doSomething() // error
AS.createInstance() // error
A.doSomething() // good
A.createInstance() // good
static abstract class
meets the requirement of handling all possible errors, just like abstract class
does.
function useDeserialize<T extends Serializable>( obj: Object, serializable: { deserialize(obj: Object) }: T ): T { return serializable.deserialize(obj); }
... this doesn't seem to be valid syntax, am I missing something?
@RyanCavanaugh
While I needed to come up with a workaround, I created a basic sample demo here, or code below. It seems that if the meaning of static
is to be truly honored, then each class or sub-class must be forced to have its own implementation in the whole inheritance chain -- otherwise it's not truly class static. So, my conclusion is that only the abstract static
method declarations should be visible across the hierarchy, but the implementations should not be inherited.
interface IPopulable {
/* static */ population: number; // abstract static -- to keep track across the class type
name: string;
}
// Simple use cases:
abstract class Animal /* implements IPopulable */ {
static population = 0;
constructor() {
Animal.population += 1;
console.log("New Animal census: ", Animal.population);
}
}
class Dog extends Animal /* implements IPopulable */ {
static population = 0;
constructor() {
super();
Dog.population += 1;
console.log("New Dog census: ", Dog.population);
}
}
class Cat extends Animal /* implements IPopulable */ {
static population = 0;
constructor() {
super();
Cat.population += 1;
console.log("New Cat census: ", Cat.population);
}
}
console.clear();
new Cat();
new Cat();
new Cat();
new Dog();
new Dog();
new Dog();
new Dog();
new Dog();
console.log(Animal.population === 8);
console.log(Dog.population === 5);
console.log(Cat.population === 3);
// polymorphic access
function getCensusFor(classType: IPopulable) {
console.log(`Current ${classType.name} census: `, classType.population);
}
console.log("Censing cat : ");
getCensusFor(Cat);
// Potential issues and solutions
// 2 ways to resolve derived class issues:
// Either this should be an compile-time error -- to not implement abstract static members from the inheritance hierarchy
class SnowCat extends Cat { /* does not implement IPopulable but it should have */
// Compile time error not implement the abstract static interface in each child-class.
}
// Or, it should be an error to allow it to be invoked
console.log("Invalid result -- it should be restricted");
getCensusFor(SnowCat); // Sould be an error -- SnowCat doesn't implement IPopulable, and should not use parent's implementation as not all Cats are SnowCats
It seems restricting abstract static
implementation purely to the only one class which implements it might solve the problem.
Most helpful comment
Since I also just stumbled on this, here is my wishes from a user-perspective. I want to be able to
abstract static
methodsHowever, 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: