`static abstract` methods and properties
See original GitHub issueThis 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 x
s?
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-classabstract
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:
- Created 4 years ago
- Reactions:495
- Comments:79 (12 by maintainers)
It’s 2022 and Q1 is almost over any update on this ? What is the timeline for it ?
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.