Open RyanCavanaugh opened 5 years ago
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
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.
i also need that, why is that not implemented ?
As usual I will request constructive engagement with the difficult problems outlined in the OP
but i can show you how i would need it:
export abstract class SerializableClass<TJson> {
constructor(json?: TJson) {
if (json) {
try {
this.fromJson(json);
} catch (exc) {
this.reset();
console.error('Error constructing from json', json, exc);
}
} else {
this.reset();
}
}
abstract reset();
abstract static fromJson(json: TJson);
fromJson(json: TJson) {
SerializableClass.fromJson(json);
}
abstract toJson(): TJson;
}
export class Point extends SerializableClass<IPoint> {
static fromJson(json: IPoint) {
const ret = new Point();
ret.fromJson(json);
return ret;
}
}
additional to the fromJson of the instance i want the fromJson as a static field on serializable classes.
of course it would be cooler if i could create an instance of the subClass in the parent class like:
export abstract class SerializableClass<TJson> {
static fromJson(json: TJson) {
const ret = new this(); // super cool, create instance of the actual this, meaning the subClass
ret.fromJson(json);
return ret;
}
}
I also need this feature badly, I have a bunch of classes that require an async constructor, which I solve like this:
class Car {
private constructor(...) {
}
public static async create(...) {
// async work
return new Car(...);
}
}
I am currently creating an abstract
class definition for all of them, but I can't type the .create
even though I need the type safety that they all take the same arguments π’
It seems restricting abstract static implementation purely to the only one class which implements it might solve the problem.
From @b4git seems viable for me ππ»
@RyanCavanaugh I would love to see progress on this, so I'm going to throw out an Option 5 proposal. Feel free to tell me if it's dumb. Top line is that the proposal depends on extending how the compiler understands typeof AClassToken
, by introducing modifiers that affect member access on class tokens. I have no idea if such an extension is possible, but it is similar in principle to how readonly
affects class and interface property declarations. I don't believe the following proposal breaks anything.
Introduce abstract static
as a valid declaration for members inside an abstract class declaration. Concrete sub-classes must implement a static property with a matching type to the abstract static
member. To facilitate desired use cases, implement the following supporting features.
abstract
keyword now also doubles as a type/access modifier for any typeof ACLassToken
For a class declared abstract class A {}
, typeof A
is an alias of abstract typeof A
. The abstract
keyword (very much open to a different name for this), used as such, is a modifier affecting member access. It is an error to access abstract static
members from an object of type abstract typeof AClassToken
. Such members simply do not exist on instances of these types.
concrete
A new keyword concrete
(again, open to other names), may act as both a type definition modifier and a member declaration modifier in the following manners.
concrete
as a type definition modifierFor a concrete class declared class B {}
, typeof B
is an alias of concrete typeof B
. For a concrete sub-class declared class B extends A { ... }
, concrete typeof B
extends concrete typeof A
, which in turn extends abstract typeof A
. Any type concrete typeof A
may safely access members declared abstract static
on abstract typeof A
.
concrete
as a member declaration modifierInside an abstract class declaration abstract class A { ... }
, members may be declared concrete static
. Such members contain bodies and may only be accessed on objects of type concrete typeof A
. For concrete static
members, this
is of type concrete typeof A
. As such they have access to all abstract static
members via this
.
Let's start by looking at the top level abstract class declaration:
// Under the proposal 'typeof A' is an alias of 'abstract typeof A', because the class is declared as 'abstract'
abstract class A {
// OK: the new 'abstract static' syntax
abstract static doSomething(): void
// OK: the new 'concrete static' syntax; 'concrete' may only be used in 'abstract' class declarations and only with the 'static' keyword
concrete static useDoSomething(): void {
// OK: inside 'concrete static' member 'typeof this === concrete typeof A'
this.doSomething()
}
// static cantDoSomething(): void {
// // Error: inside a non-'concrete' static member, so 'typeof this === abstract typeof A', so the 'abstract static' member doesn't exist
// this.doSomething()
// }
}
Here we see basic use of abstract static
and how the concrete
keyword can be used to modify the type of this
.
Using the token for the class we just defined (assuming, we see the issue in Option 3 is solved:
// Error: 'abstract static' member does not exist on 'abstract typeof A'
A.doSomething()
let x = A
// Still an error: 'typeof x === abstract typeof A', so 'abstract static' member doesn't exist
x.doSomething()
// Another error: 'concrete static' member also does not exist on 'abstract typeof A'
A.useDoSomething()
Tokens carry the abstract
and concrete
modifiers with them, so no dread inconsistency.
Now lets make concrete sub-classes and really see concrete
and abstract
in action.
// Error: E must implement 'abstract static' member 'doSomething'
class E extends A {
}
// OK: abstract static member implemented; under the proposal 'typeof B = concrete typeof B', because the class was not declared 'abstract'; concrete typeof B extends concrete typeof A extends abstract typeof A
class B extends A {
static doSomething() {}
}
// OK: trivial use
B.doSomething()
// Also OK: 'typeof B === concrete typeof B' and 'concrete typeof B extends concrete typeof A', so 'concrete static' member exists
B.useDoSomething()
Works as expected. We can use code defined in the abstract class declaration that interacts with code defined in the concrete sub-class declaration!
Looking at functions to clarify general use cases:
// Legacy function can't use new 'abstract static' members, because 'typeof A === abstract typeof A'
function oldFn(x: typeof A) {
// // Error
// x.doSomething()
// // Error
// x.useDoSomething()
}
// Update the old function to type check with the 'concrete' modifier
function fn(x: concrete typeof A) {
// OK
x.doSomething()
// Also OK
x.useDoSomething()
}
// OK: 'typeof B === concrete typeof B' & 'concrete typeof B extends concrete typeof A'
fn(B)
// Error: 'typeof A === abstract typeof A' which conflicts with the function parameter type
fn(A)
// OK: 'concrete typeof A extends abstract typeof A'
oldFn(B)
And finally, what happens with abstract sub-classes:
abstract class C extends A {
static alsoCantDoSomething(): void {
// Error: 'typeof this === abstract typeof C' & 'abstract typeof C extends abstract typeof A', so 'doSomething' does not exist on 'this'
this.doSomething()
}
}
abstract class D extends A {
// OK: implements abstract signature properly, so here and down the inheritance chain 'doSomething' is no longer 'abstract'
static doSomething(): void {}
static canDoSomething(): void {
// OK: again the 'abstract' modifier has been stripped from 'doSomething', because it is concretely implemented
this.doSomething()
}
}
// Error: note that this is still illegal as 'typeof D !extend concrete typeof A'
D.useDoSomething()
// I don't know if this should be a concern though
I am hopeful that this is feasible, but I just don't know enough about TypeScript's engine to be sure. Again, old code shouldn't break under this proposal, but it would need to be refactored in some places to make use of abstract static
members.
Eager to get feedback on this.
Can you make a table with columns for static
, abstract static
, and concrete static
, used as member declaration modifiers, with rows showing how they differ? I'm not quite clear on why all 3 are needed. For example, why does the declaration of D
in the last block have the methods as static
instead of concrete static
? Would concrete static
be incorrect, or would it limit how the methods can be invoked?
Here you go!
condition | static member | abstract static member | concrete static member |
---|---|---|---|
typeof this is abstract typeof A |
β | (body disallowed) | β |
typeof this is concrete typeof A |
β | (body disallowed) | β |
typeof this extends abstract typeof A |
β | (body disallowed) | β |
exists on abstract typeof A |
β | β | β |
exists on concrete typeof A |
β | β | β |
On class D
in the last block, you can declare the methods as concrete static
, but then the following would produce errors:
// Errors: if we declared doSomething and canDoSomething as concrete static, not just static
D.doSomething()
D.canDoSomething()
let x = D
x.doSomething()
You need the concrete static
modifier to enable safely accessing abstract static
members from within an abstract class declaration. However, if you don't actually use any abstract static
members within concrete static
member bodies, you don't gain anything except limited use of the member you're declaring. It would be a bit like declaring a method on a class, but not using this
in the method. Perfectly safe, but not useful in any real sense.
Reviewing the thread, I realized my proposal is likely functionally the same as @SploxFox's, but with two advantages. First, the concrete typeof Foo
syntax is more terse than Implemented<typeof Foo>
. Second, concrete static
declarations eliminate the clunky and verbose this: Implemented<typeof Foo>
parameter specification in static methods that need abstract static
access.
I'm not sure soo much logic and custom syntax is actually required to provide the required features.
Here's an example on how I solved the problem of static class abstraction. For that I'm using a trick I found online which uses decorators to enforce an interface on static class members:
/** class decorator to force static fields to be implemented */
export function StaticImplements<T>() {
return (constructor: T) => { /* nop */ };
}
To make a class then implement a static interface, you would use it like this:
interface InstanceInterface {
serialize(): any;
}
interface StaticInterface {
serializedName: string;
}
@StaticImplements<StaticInterface>()
class MyClass implements InstanceInterface {
static serializedName = 'myClass';
serialize() { return undefined; }
}
And when requesting a class type as an input for a method, current typescript already provides everything required:
interface ClassType<T = any, ARGS extends any[] = any[]> {
new(...args: ARGS): T;
}
function test(clazz: ClassType<InstanceInterface> & StaticInterface) {
// ...
}
test(MyClass);
So the only thing I think would be required would be an alternative to using the decorator to enforce static interfaces onto a class. I could imagine something like this instead:
class MyClass implements InstanceInterface, static StaticInterface {
static serializedName = 'myClass';
serialize() { return undefined; }
}
This would also solve an issue I see with other proposals here which afaik do not allow specifying multiple static interfaces for a class, because they depend on abstract class semantics which I do not think are suited here.
I would like to contribute my small hack. Whenever I start getting a set of fields that any sub class must implement, I do something like this
abstract class CustomError {
static code: string
static define(errorCode: string) {
return class extends CustomError {
static code = errorCode
}
}
}
class NotFoundError extends CustomError.define('not-found') {}
I tend to use this also to emulate the lack of "final classes" by convention, like:
const NotFoundError = CustomError.define('not-found')
I would like to add my own request for this feature. It would be really useful for avoiding the need to initialize classes when you only need information, not functionality. It would also help when developers are trying to use the Abstract factory method.
As an example, let's say I have a list of developers and I want to print their names:
abstract class Person {}
class Bruce extends Person {}
class Ryan extends Person {}
class Mauricio extends Person {}
function printDevelopers() {
const bruce = new Bruce();
const ryan = new Ryan();
const mauricio = new Mauricio();
console.log('We would like to thank the following developers:');
for (let person of [ bruce, ryan, mauricio ]) console.log('+ ' + person.fullname);
}
The problem with that is that it requires creating a new instance of each person, which is a really big waste.
I would prefer something like this:
function printDevelopers() {
const developers = [ Bruce, Ryan, Mauricio ];
console.log('We would like to thank the following developers:');
for (let person of developers) console.log('+ ' + person.fullname);
}
Technically, you can already do this by declaring a static property in the abstract class and then overriding it in each subclass. But that sacrifices type-safety. The whole point of type checking is to catch programmer errors at compile-time, and forgetting to override a property from an abstract class seems like a compile-time programmer error to me.
I might have another proposal for how implementation should work. Since abstract classes are only enforced at compile-time, why not compile abstract static properties to an undefined get
property?
For example, this code:
abstract class Animal {
static abstract species: string;
static abstract isBipedal(): boolean;
}
class Cat extends Animal {
static species: string = 'Felis catus';
static isBipedal() { return false; }
}
class Human extends Animal {
static species: string = 'Homo sapiens';
static isBipedal(): { return true; }
}
would be compiled into something like this:
class Animal {
static get species() { return undefined; }
static get isBipedal() { return undefined; }
}
class Cat {
static get species() { return 'Felis catus'; }
static isBipedal() { return false; }
}
class Human {
static get species() { return 'Homo sapiens'; }
static isBipedal() { return true; }
}
Any thoughts?
Okay, so I have looked at all of the suggestions in the original post, and all of the comments on this thread and the preceding thread, and I think they are all looking at this from the slightly wrong perspective, except for @RyanCavanaugh's suggestion here, (edit, I also see now that I missed @olee's very similar suggestion here) which I like, and I think it will work, but I think it is missing a fundamental issue.
We must remember that JavaScript uses prototype inheritence, rather than class inheritence, and right now, typescript's syntax greatly emphasizes traditional class style inheritance, when this is not at all what is going on. I think this is the main source of all of the confusion, and although from a functional perspective the suggestion above works, it continues to emphasize this way of thinking, which will only lead to more confusion. We are trying to shove higher order inheritance into 0th order inheritance, which is like trying to fit a square peg in a round hole. The problem of specifying types of types is not new. Although not common in most popular languages and even in academic languages, it is well studied.
Form a type theory perspective, what I think we want here are [kinds](https://en.wikipedia.org/wiki/Kind(typetheory)). Right now, essentially non primitive types all have the same kind, which is Function
. If we restrict ourselves to the higher order parts of typescript's type theory, this is very much like saying all types have the type Object, and nothing more specific than that, which isn't very type safe if we are trying to implement static contracts, which in javascript because of its prototypal nature, manifests itself as exactly this. To be more sepcific:
@RyanCavanaugh suggestion, which I think is the best so far, looks like this
class WidgetMaker implements Factory, static FactoryCtor {
// ...
}
and for abstract classes I imagine it would look something like this
class WidgetMaker extends static FactoryCtor {
// ...
}
Again, I think this is functional and is the best suggestion so far, but I think this is making people uncomfortable for the simple reason that by looking at the definition FactoryCtor
alone, it is unclear that one is supposed to extend/implement it statically, which could lead to confusion, if say someone unknowingly wrote
class WidgetMaker extends FactoryCtor {
// ...
}
This would produce no error, and the developer would likely go through the painstaking trouble of implementing FactoryCtor non statically before trying to pass their WidgetMaker into some method that expects it to have static implementations instead.
Instead, I would propose that there should be some mechanism for asserting that FactoryCtor is a first order kind, while WidgetMaker is a 0th order kind (aka type or Function, which is automatically inferred when you use interface, class, type, or abstract class, to create a type), but which has first order kind FactoryCtor.
In this specific use case, this would look like this
abstract kind AbstractAdder { // Has kind `kind`, which extends some superkind representing Function (like all classes extend Object)
abstract add1 : (n : number) => number,
add2 = (n) => n + 2
}
class Adder : AbstractAdder { // Has kind AbstractAdder rather than kind type
// Compiler has error since Adder does not implement add1 as a static method
}
class Adder : AbstractAdder {
static add1 = (n) => n + 1 // Okay, add2 is already implemented on AbstractAdder
}
Furthermore, this avoids the problems mentioned in the original post, and many others since AbstractAdder is now a kind (a type of a type) rather than a type.
AbstractAdder.add1() // Compiler error, add1 is not static on kind
class Adder extends AbstractAdder { ...} // Compiler error, AbstractAdder is not a type
typeof AbstractAdder // Compiler error, AbstractAdder is of higher kind, so typeof doesn't make sense, since it is itself a type.
abstract kind Complicated {
abstract setup(): void;
abstract print(): void;
abstract ship(): void;
abstract shutdown(): void;
}
function fn(x: typeof Complicated) { // Compiler error here, since typeof does not make sense, it is a kind.
// won't get this far
x.setup();
// won't get this far
x.print();
// won't get this far
x.ship();
// won't get this far
x.shutdown();
}
function fn(x : Complicated) {
//No error, Complicated can't be of kind Complicated, there is a strict heirarchy of kinds
x.setup();
x.print();
x.ship();
x.shutdown();
}
function fn2<T extends Complicated>(x : T) {
x.setup() // No compiler error. T is inferred to be a second order kind
}
// Perhaps, new syntax could be added to support this with generics, now generics can have a kind specified, and
// implementations of that kind can be called through the generic. I don't think this would require any changes to the runtime
// and could be statically validated (although I am not 100% sure about this)
function fn3<V extends Complicated, T extends Whatever : V>(x : T, y : V) {
y.setup(); // No errors
x.setup(); //Compiler error
}
The usecase for generics having explicit kinds is similar to the use cases of Java allowing the ability to pass in a Class representing the type T. For example, a nice way of representing a deserializable class from JSON, similar to how Java does things.
abstract kind Parseable<T : Parseable<T>> { // Not circular since T is a type, not a kind
readonly dirnamePath : string; //Set to __dirname by the user
readonly validate : (str : string) => void
constructor(dirnamePath: string, t : typeof T) {
this.dirnamePath = dirnamePath;
this.validate = // lambda that gets auto generated JSON schema guaranteed unique combination of t.name and __dirname
}
public parse(str: string) : T {
this.validate(str);
return JSON.parse(str) as T;
}
}
class Animal : Parseable {
// Will get compiler error below if second argument is not Animal, or if this constructor isn't here
// Since Parseable can only be instantiated as a type (i.e. implemented by a class)
// We know that if our schema generator is working properly, then either we will get an error
// at startup if the first argument is wrong or a compiler error if the second argument is wrong.
static construct(__dirname, Animal);
}
// We know that T must itself implement parseable<T>, so we now ban
// JSON.parse with our linter from our codebase and never worry about
// parsing breaking type safety without even throwing runtime exceptions ever again :)
function parse<T : Parseable<T>>(json: string, x : Parseable<T>) : T {
return x.parse(json);
}
Transpiling non abstract methods of a kind to javascript could be accomplished in a multitude of ways, but is probably the biggest design challenge. Probably the simplest thing would be to transpile the Kinds themselves exactly the same way that it is done for abstract classes, and when a class instantiates the kind, just do
Object.assign(MyClass, new MyKind(/*...args*/));
passing in the arguments to the constructor (which by their nature have to be static)
It may be helpful to summarize the ideal specs of this feature request, or at least try to push closer to a consensus. As I see it, abstract static
functionality means (1) a way to mandate static
member implementation in non-abstract child classes, (2) the ability to define functionality that accesses such members given the signature generated by (1), and (3) achieving (1) & (2) within the same construct so as to facilitate reusability of (2) through the inheritance chain.
@olee's suggestion here can be useful. However, it only achieves (1) & (2), not (3). It is up to the user base and TS dev team to decide if this is sufficient. Personally, I feel like this is a lot of friction and indirection to partially achieve what feels like a very straightforward goal.
@Wikiemol's suggestion above achieves (1), (2), & (3) in concept. I have a couple critiques though. First, the addition of a higher-order type would be a substantial increase in language complexity, if not for the compiler engine, then certainly for the average user. abstract static
is the requested feature, because it fits squarely within developers' current mental model. Reaching for complex solutions that are far outside that model is ill advised, at least without an extremely compelling rationale. My other critique, and this is very much nitpicking, kind
is a problematic as a new language keyword. It is common practice to use a kind
field in type-discrimination. Moreover, Kind
is commonly enough littered through type names in various codebases, which would generate confusion if the concept of a higher order kind type were introduced.
My suggestion here achieves (1), (2), & (3) with the advantages that it conforms to the mental model of abstract static
through the symmetric concrete static
statement, it has a fairly clear and concise path to implementation (the compiler appears to already track whether or not a typeof X
symbol is abstract
), and it has advantageous side affects.
EDIT: After more consideration, I realized the following example cannot work. It breaks down when you consider importing classes from third party packages. I can explain further if anyone is interested, but for now I just want to prevent confusion on the question. I'm leaving the example intact to preserve context.
~~ Begin Broken Example
Consider how the proposed concrete
modifier works on properties of an abstract class, not just methods:
abstract class Ford2021Suv {
concrete static assemblyLine = new Ford2021SuvAssemblyLine(); // Line 1
concrete engine = new Ford2021SuvEngine(); // Line 2
}
class FordExplorer2021 extends Ford2021Suv {
}
const vin123 = new FordExplorer2021();
From Line 1: each concrete subclass of Ford2021Suv, such as FordExplorer2021, gets its own assembly line instance. I don't know of any existing simple pattern than replicates this capability. From Line 2: each concrete instance of Ford2021Suv
, such as vin123, gets its own engine instance. This second effect is easy to achieve currently, but would require four lines:
abstract class Ford2021Suv {
engine: Ford2021SuvEngine;
constructor() {
this.engine = new Ford2021SuvEngine();
}
}
~~ End Broken Example
Finally, to clear up the point that (I think) @Wikiemol alluded to and has been a source of confusion in past comments: static
members in JavaScript do not behave in the traditional OOP way, because of the prototype chain. Specifically, a static class member in most OOP languages exists solely on the symbol of the class in which it was initially declared. In JavaScript, because of the inheritance chain, every subclass of a class with a static member inherits the same static class member. TypeScript, of course, inherits this behavior and must abide by it. My proposal is in keeping with this behavior, except that abstract static
and concrete static
members exist on the first concrete subclass on down, not on the abstract class in which they were declared/defined. @Wikiemol's suggestion sidesteps this issue entirely with the introduction of higher order kinds.
@GusBuonv I have been trying to implement a basic form of my suggestion for the past few days after work, and I definitely agree with you about the introduction of the kind keyword. However, I pivoted to a different idea for the keyword itself. Instead of introducing an entirely new keyword, locally I have added the ability to append a caret token ^ to class and interface, which specifies the order of the type.
E.g.
class^ A {
field: string = 'value';
}
abstract class^ A {
field: string = 'value';
}
interface^ A {
field: string = 'value';
}
Really the caret symbol could be something else, but I think it opens the door for arbitrarily high orders with a simple extension of the syntax
class^n A {
field: string = 'value';
}
abstract class^n A {
field: string = 'value';
}
interface^n A {
field: string = 'value';
}
And also opens the door for adding this to the type
keyword (although I am not quite sure why we would ever need to do that, I think it could probably be inferred or banned in most cases).
I also agree that it may be better to use something closer to these being seen as classes, and I think this helps with that.
Furthermore, I got the following test to pass relatively easily, from a code perspective, it was done by simply adding a typeOrder
field to the corresponding types in the code base. Right now I have
class^ A1 {
field: string = 'value';
}
class^ B1 extends A1 {
field: string = 'value';
}
interface^ InterfaceA1 {
parentField: string;
}
interface^ InterfaceB1 extends InterfaceA1 {
childField: string;
}
class^ ClassB1 implements InterfaceB1 {
field: string = 'value';
childField: string = 'value';
parentField: string = 'value';
}
abstract class^ AbstractClassB1 implements InterfaceB1 {
field: string = 'value';
abstract childField: string;
abstract parentField: string;
}
function validClassExpression1() {
const ValidExpression1 = class^ {
}
return ValidExpression1;
}
class^ ExtendsValidClassExpression1 extends validClassExpression1() {
}
function validClassExpression0() {
const ValidExpression0 = class {
}
return ValidExpression0;
}
class ExtendsValidClassExpression0 extends validClassExpression0() {
}
class A0 {
field: string = 'parent'
}
interface G0 {
field: string;
}
// No errors up until here
class^ C1 extends A0 { // A Class cannot extend a class of another order.
field: string = 'value';
}
class^ D1 {
field: string = 'value';
}
class B0 extends D1 { // A Class cannot extend a class of another order.
field: string = 'value';
}
class E0 implements InterfaceB1 { // A Class cannot implement an interface of another order.
field: string = 'parent'
childField: string = 'value';
parentField: string = 'value';
}
interface F0 extends InterfaceA1 { // An interface cannot extend an interface of another order.
childField: string;
}
interface^ G1 extends G0 { // An interface cannot extend an interface of another order.
childField: string;
}
class TiredOfNamingThese extends AbstractClassB1 { // A class cannot extend a class of another order
field: string = 'value';
childField: string = '';
parentField: string = '';
}
function classExpression0() {
const Expression0 = class {
}
return Expression0;
}
class^ ExtendsClassExpression0 extends classExpression0() { // A class cannot extend a class of another order
}
function classExpression1() {
const Expression1 = class^ {
}
return Expression1;
}
class ExtendsClassExpression1 extends classExpression1() { // A class cannot extend a class of another order
}
Still working on trying to prototype the : syntax, but that seems relatively straightforward, since we can reuse the existing code for checking base type implementations but just use typeof
instead.
I do think your critique about this making things more complicated from a user perspective is a valid one. But I think from a code perspective it has actually been relatively easy to implement, since really this is just a 'higher order' version of the same stuff that already exists.
@GusBuonv I agree that your abstract static could be easy to understand, but it has a basic flaw which makes it impossible to use as a solution for this problem. Instead I would agree that the static-implements proposal from @RyanCavanaugh which can be faked with decorators as I showed here is a very easy to understand solution while providing all the required features.
As for why your solutin @GusBuonv cannot be used, let me show an example with the static-implements syntax which cannot be expressed with the abstract static proposal:
// let's say in some library you have no control over, the following class is exported
export class UserManager {
// ...
}
// then in your own code, you extend this class and want to integrate it in some let's say DI framework which can manage and instantiate classes when they have a static create method
export interface ICreatable<T> {
create(): T;
}
export class MyUserManager extends UserManager implements static ICreatable<MyUserManager> {
public static create() {
return new MyUserManager(...);
}
}
export function createType<T>(type: ICreatable<T>): T {
const res = type.create();
assert(res instanceof type);
return res;
}
This kind of code would not be implementable with the abstract class syntax for various reasons:
createType
function, because you would have to provide it all kinds of abstract base classes you might want to instantiate - this is why an interface would be optimal@olee I do think this is the best proposal so far, but I really think it is somewhat of a misnomer to say that this provides all the required features. I truly believe that perhaps people aren't communicating the full extent of what they want out of this feature, and I think there is a reason they aren't satisfied with any of the solutions that already exist in the language. I don't think syntactic sugar for what already exists is all of what people really want.
I say this only because I don't think implementing static implements
as proposed would actually stop all of the requests related to this. Whether it is explicitly acknowledged or not, "static implements" is a form of specifying a "type of a type", that is what it is. And there are many other languages which have done this and done this well. Forcing a type to implement static methods is exactly what typeclass
is in Haskell, and is exactly what trait
is in Scala. In these languages, typeclasses and traits are explicitly different than regular types. The problems of not implementing a "static implements" in the way that these other languages have done it are made even worse by the fact that things in typescript can have multiple types (not a bad thing, I am just saying that it makes the problem even harder, and needs to be handled with a bit more care). Obviously, Ryan Cavanaugh's issues with the initial proposals of this are some of the reasons why this is made harder, but I don't believe these are the only problems, and if they are now, I don't think it will remain that way in the future.
As type level programming in typescript becomes more complicated, I believe that it is important to treat the "higher order" parts of typescript with the same level of care that the lower order parts have been treated. static implements
does not give types an explicit kind, it doesn't really give a type a kind at all, and this is a problem, because that is what "static implements" is really supposed to be doing. The kind of a type becomes ambiguous, and I believe it will lead to more confusion. It seems simpler now, but I don't think it will stay that way as more proposals come in and more features are added to the language in the future.
Obviously, since we are all using Typescript, and not JavaScript, we all understand the importance of having explicit types for things, even if there isn't one type, we understand the importance of a type not being ambiguous. The same applies for the higher order parts of a type system. This I think is really what people want. They want a concept in the language that guarantees that a type has some explicit structure, and they want to abstract over this. Static contracts are just one way in which this desire manifests itself.
The idea here is that you are either specifying the structure of a type or the structure of an instance. If we look at all of the use cases on all of these tickets, none of them give examples that would ever need to be both at the same time, this is because doing so leads to confusion and inconsistency, and is also unnatural.
Furthermore, the distinction between statically implementing things and not is actually very important for the correctness of your code in many cases, for example, Java made this famous mistake when creating their Comparable interface (see here and here). The thing here is that if you extend a class T
which extends Comparable, and override compareTo
, then now, sorting a list List<T>
using compareTo can have very unexpected behavior. Obviously, the solution here is to have a single method that you use to sort which you pass in to the sort method. In Java, this manifests itself with the Comparator interface, but if you have ever worked in a large code base with lots of these comparators, it is a nightmare, because you don't know which Comparators correspond to which types, and when you are supposed to use them and what they do. Moreover, if you are say, sorting a list of comparables with a comparator, then it becomes unclear from the types which the sort function is supposed to be using (it would obviously be really dumb and unlikely for someone to create a function signature like this in this case, but in more complicated cases, the likelihood for this kind of confusion increases very fast)
This is just the simplest possible interface which only has one method, but now if you imagine interfaces like this that need to be implemented statically but have more than one method this problem gets even worse. And the fact that typescript defines a subtype not by an explicitly declared type, but by the properties that are on a given value, makes this even more complicated still (there aren't too many other languages like this, and again this is not a bad thing, in fact it is one of my favorite things about typescript, but its just another way in which this complicates things)
Consider, also, a point which @RyanCavanaugh brought up in #44875, which is that people will want this behavior for arbitrary types and not just classes (I think I have actually even seen this request somewhere in the issue tracker already, but I can't find it now). In this case
type MyType static implements MyInterface = ....
seems really misleading, verbose, and unnatural to me whereas
type MyType: MyInterface = ....
brings to the programmers mind the concept that you are initializing a variable, which is really what you are doing in all of these cases (and what it would be doing when compiled to Javascript).
Similarly, in the alternative syntax for assigning anonymous classes to variables
const MyClass: MyStaticInterface = class {
...
}
makes a lot more sense to me personally.
It is really only the particular case of using the es6 syntax for classes where static implements
makes sense. Prior to es6, this wouldn't have made any sense at all. Classes in Javascript don't really exist at all, and I think its important we remember that.
I agree that my proposal might be a little complicated (not the best at coming up with user friendly stuff), and I can fully understand if that leads to its immediate dismissal. But hopefully, you can at least understand the reasons I am bringing it up.
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: