microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.06k stars 12.29k forks source link

`static abstract` methods and properties #34516

Open RyanCavanaugh opened 4 years ago

RyanCavanaugh commented 4 years ago

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();

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();

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.

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

Option 5: Something else?

Anyone able to square this circle?

Checklist

My suggestion meets these guidelines:

AlCalzone commented 4 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.

thw0rted commented 4 years ago

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?

thw0rted commented 4 years ago

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?

fatcerberus commented 4 years ago

[...] 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.

thw0rted commented 4 years ago

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.

fatcerberus commented 4 years ago

That would likely fall under type-directed emit and therefore is a nonstarter.

dragomirtitian commented 4 years ago

@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 😊.

thw0rted commented 4 years ago

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

dragomirtitian commented 4 years ago

@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

CreatedBySeb commented 4 years ago

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
CreatedBySeb commented 4 years ago

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.

eddiemf commented 4 years ago

No more progress on this issue? It's been 3 years already 😞

RyanCavanaugh commented 4 years ago

@eddiemf which of the five proposals listed in the OP do you think we should be progressing on, and why?

eddiemf commented 4 years ago

@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

IsaiahByDayah commented 4 years ago

Commenting for future update notifications. Would love this feature as well. Also looking to enforce implementation of the method in subclasses πŸ˜…

manzt commented 4 years ago

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);
minecrawler commented 4 years ago

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

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
thw0rted commented 4 years ago

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)"?

arantes555 commented 4 years ago

@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>) => {}
thw0rted commented 4 years ago

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.

arantes555 commented 4 years ago

@thw0rted check this

jcalz commented 4 years ago

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.

thw0rted commented 4 years ago

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

RyanCavanaugh commented 4 years ago

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();
SerenaLynas commented 4 years ago

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?

JasonHK commented 4 years ago

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.

JasonHK commented 4 years ago

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.

JasonHK commented 4 years ago

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?

SerenaLynas commented 4 years ago

@JasonHK Oops. Fixed it

dinofx commented 3 years ago

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.

charles-toller commented 3 years ago

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.

millsp commented 3 years ago

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

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.

ericblade commented 3 years ago
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?

b4git commented 3 years ago

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

nickwinger commented 3 years ago

i also need that, why is that not implemented ?

RyanCavanaugh commented 3 years ago

As usual I will request constructive engagement with the difficult problems outlined in the OP

nickwinger commented 3 years ago
  1. this is one year old
  2. it is easy to implement
  3. i guess there are enough arguments already

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;
 }
}
lenovouser commented 3 years ago

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 πŸ‘πŸ»

GusBuonv commented 3 years ago

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

Proposal:

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.

Supporting Feature 1: 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.

Supporting Feature 2: new keyword 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 modifier

For 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 modifier

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

Examples

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

Final Thoughts

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.

thw0rted commented 3 years ago

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?

GusBuonv commented 3 years ago

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.

GusBuonv commented 3 years ago

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.

olee commented 3 years ago

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.

fgarcia commented 3 years ago

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') 
Symbitic commented 3 years ago

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?

Wikiemol commented 3 years ago

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)

GusBuonv commented 3 years ago

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.

Wikiemol commented 3 years ago

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

olee commented 2 years ago

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

Wikiemol commented 2 years ago

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