Typescript: `static abstract` methods and properties

Created on 16 Oct 2019  路  34Comments  路  Source: microsoft/TypeScript

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:

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.

Use Cases

(what are they?)

Unresolved Questions

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 xs?

Option 1: All of them

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();
  • Pros: Ergonomic
  • Cons: Literally allows the runtime-crashing code A.doSomething(), which seems like a fairly large design deficit

Option 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:

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();
  • Pros: Correctly prevents all crashes
  • Cons: Extremely unergonomic at use cases; effectively bans concrete static methods from calling same-class abstract methods

Option 3: Indirection is sufficient

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

  • Pros: Catches "bad by inspection" instances while still allowing "maybe it works" code
  • Cons: Extremely inconsistent; simply appears to function as if TypeScript has a bug in it. Unclear what sufficient indirection means in cases of e.g. module imports

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:

// 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".

  • Pros: Maybe a slightly better variant of option 3
  • Cons: Just a more complicated system with the same failure modes

Option 5: Something else?

Anyone able to square this circle?

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Needs Proposal Suggestion

Most helpful comment

Since I also just stumbled on this, here is my wishes from a user-perspective. I want to be able to

  • write abstract static methods
  • call non-abstract methods on the abstract class from outside the same class
  • call abstract methods from non-abstract methods inside the same class

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.

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

All 34 comments

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 newable 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 

Play

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;
}

Play

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

  • write abstract static methods
  • call non-abstract methods on the abstract class from outside the same class
  • call abstract methods from non-abstract methods inside the same class

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.

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 newing 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
    Only allows static members, can be used anytime.
  • static abstract class
    Allows both 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbondc picture jbondc  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

wmaurer picture wmaurer  路  3Comments

uber5001 picture uber5001  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments