question-mark
Stuck on an issue?

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

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

`static abstract` methods and properties

See original GitHub issue

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:

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

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:495
  • Comments:79 (12 by maintainers)

github_iconTop GitHub Comments

46reactions
Shereefcommented, Oct 6, 2022

It’s 2022 and Q1 is almost over any update on this ? What is the timeline for it ?

23reactions
oleecommented, Aug 8, 2021

I fully agree with @GusBuonv and I think we should try to focus on the actual proposal which I agree would be really nice to have.

Read more comments on GitHub >

github_iconTop Results From Across the Web

c# - Can't define static abstract string property - Stack Overflow
I have an abstract class that contains a static method that accepts a static string that I would like to define as an...
Read more >
Static abstract methods in interfaces - C# 11.0 draft ...
Abstract static members ... Static interface members other than fields are allowed to also have the abstract modifier. Abstract static members are ...
Read more >
Static Abstract Members In C# 10 Interfaces | Khalid Abuhakmeh
Static abstract members allow each implementing member of an interface to implement their version of a static accessor that you can access via ......
Read more >
Abstract Static Methods in C#: Do They Exist in 2022?
Abstract methods and static methods are key concepts in C# object-oriented programming. · If a static method is declared as abstract, then the...
Read more >
C# 11 static abstract members - NDepend
static abstract members cannot be made virtual. This makes sense because virtual / override really means: take account of the type of the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

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

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found