microsoft / TypeScript

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

Suggestion: Add abstract static methods in classes and static methods in interfaces #14600

Closed vyshkant closed 5 years ago

vyshkant commented 7 years ago

As a continuation #2947 issue, which allows the abstract modifier on method declarations but disallows it on static methods declarations, I suggest to expand this functionality to static methods declarations by allowing abstract static modifier on method declarations.

The related problem concerns static modifier on interface methods declaration, which is disallowed.

1. The problem

1.1. Abstract static methods in abstract classes

In some cases of using the abstract class and its implementations I may need to have some class-dependent (not instance-dependent) values, that shoul be accessed within the context of the child class (not within the context of an object), without creating an object. The feature that allows doing this is the static modifier on the method declaration.

For example (example 1):


abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

FirstChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class FirstChildClass'
SecondChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class SecondChildClass'

But in some cases I also need to acces this value when I only know that the accessing class is inherited from AbstractParentClass, but I don't know which specific child class I'm accessing. So I want to be sure, that every child of the AbstractParentClass has this static method.

For example (example 2):


abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

abstract class AbstractParentClassFactory {

    public static getClasses(): (typeof AbstractParentClass)[] {
        return [
            FirstChildClass,
            SecondChildClass
        ];
    }
}

var classes = AbstractParentClassFactory.getClasses(); // returns some child classes (not objects) of AbstractParentClass

for (var index in classes) {
    if (classes.hasOwnProperty(index)) {
        classes[index].getSomeClassDependentValue(); // error: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.
    }
}

As a result, the compiler decides that an error occurred and displays the message: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.

1.2. Static methods in interfaces

In some cases, the interface logic implies that the implementing classes must have a static method, that has the predetermined list of parameters and returns the value of exact type.

For example (example 3):

interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable; // error: 'static' modifier cannot appear on a type member.
}

When compiling this code, an error occurs: 'static' modifier cannot appear on a type member.

2. The solution

The solution to both problems (1.1 and 1.2) is to allows the abstract modifier on static method declarations in abstract classes and the static modifier in interfaces.

3. JS implementaion

The implementation of this feature in JavaScript should be similar to the implementation of interfaces, abstract methods and static methods.

This means that:

  1. Declaring abstract static methods in an abstract class should not affect the representation of the abstract class in the JavaScript code.
  2. Declaring static methods in the interface should not affect the representation of the interface in JavaScript code (it is not present).

For example, this TypeScript code (example 4):


interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable;
}

abstract class AbstractParentClass {
    public abstract static getSomeClassDependentValue(): string;
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass implements Serializable {

    public serialize(): string {
        var serialisedValue: string;
        // serialization of this
        return serialisedValue;
    }

    public static deserialize(serializedValue: string): SecondChildClass {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    }

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

should be compiled to this JS code:

var AbstractParentClass = (function () {
    function AbstractParentClass() {
    }
    return AbstractParentClass;
}());

var FirstChildClass = (function (_super) {
    __extends(FirstChildClass, _super);
    function FirstChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    FirstChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class FirstChildClass';
    };
    return FirstChildClass;
}(AbstractParentClass));

var SecondChildClass = (function (_super) {
    __extends(SecondChildClass, _super);
    function SecondChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    SecondChildClass.prototype.serialize = function () {
        var serialisedValue;
        // serialization of this
        return serialisedValue;
    };
    SecondChildClass.deserialize = function (serializedValue) {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    };
    SecondChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class SecondChildClass';
    };
    return SecondChildClass;
}(AbstractParentClass));

4. Relevant points

All the other properties of abstract static modifier should be inherited from abstract and static modifiers properties.

All the other properties of static interface method modifier should be inherited from the interface methods and static modifier properties.

5. Language Feature Checklist

zpdDG4gta8XKpMCd commented 6 years ago

although I hate the idea of having static interfaces but for all practical purposes the following should be enough today:

type MyClass =  (new (text: string) => MyInterface) & { myStaticMethod(): string; }

that can be used as:

const MyClass: MyClass = class implements MyInterface {
   constructor(text: string) {}
   static myStaticMethod(): string { return ''; }
}

UPDATE:

more details on the idea and a live [example](http://www.typescriptlang.org/play/#src=%2F%2F%20dynamic%20part%0D%0Ainterface%20MyInterface%20%7B%0D%0A%20%20%20%20data%3A%20number%5B%5D%3B%0D%0A%7D%0D%0A%2F%2F%20static%20part%0D%0Ainterface%20MyStaticInterface%20%7B%0D%0A%20%20%20%20myStaticMethod()%3A%20string%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20type%20of%20a%20class%20that%20implements%20both%20static%20and%20dynamic%20interfaces%0D%0Atype%20MyClass%20%3D%20(new%20(data%3A%20number%5B%5D)%20%3D%3E%20MyInterface)%20%26%20MyStaticInterface%3B%0D%0A%0D%0A%2F%2F%20way%20to%20make%20sure%20that%20given%20class%20implements%20both%20%0D%0A%2F%2F%20static%20and%20dynamic%20interface%0D%0Aconst%20MyClass%3A%20MyClass%20%3D%20class%20implements%20MyInterface%20%7B%0D%0A%20%20%20constructor(public%20data%3A%20number%5B%5D)%20%7B%7D%0D%0A%20%20%20static%20myStaticMethod()%3A%20string%20%7B%20return%20''%3B%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20works%20just%20like%20a%20real%20class%0D%0Aconst%20myInstance%20%3D%20new%20MyClass(%5B%5D)%3B%20%2F%2F%20%3C--%20works!%0D%0AMyClass.myStaticMethod()%3B%20%2F%2F%20%3C--%20works!%0D%0A%0D%0A%2F%2F%20example%20of%20catching%20errors%3A%20bad%20static%20part%0D%0A%2F*%0D%0Atype%20'typeof%20MyBadClass1'%20is%20not%20assignable%20to%20type%20'MyClass'.%0D%0A%20%20Type%20'typeof%20MyBadClass1'%20is%20not%20assignable%20to%20type%20'MyStaticInterface'.%0D%0A%20%20%20%20Property%20'myStaticMethod'%20is%20missing%20in%20type%20'typeof%20MyBadClass1'.%0D%0A*%2F%0D%0Aconst%20MyBadClass1%3A%20MyClass%20%3D%20class%20MyClass%20implements%20MyInterface%20%7B%0D%0A%20%20%20constructor(public%20data%3A%20number%5B%5D)%20%7B%7D%0D%0A%20%20%20static%20myNewStaticMethod()%3A%20string%20%7B%20return%20''%3B%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20example%20of%20catching%20errors%3A%20bad%20dynamic%20part%0D%0A%2F*%0D%0AType%20'typeof%20MyBadClass2'%20is%20not%20assignable%20to%20type%20'MyClass'.%0D%0A%20%20Type%20'typeof%20MyBadClass2'%20is%20not%20assignable%20to%20type%20'new%20(data%3A%20number%5B%5D)%20%3D%3E%20MyInterface'.%0D%0A%20%20%20%20Type%20'MyBadClass2'%20is%20not%20assignable%20to%20type%20'MyInterface'.%0D%0A%20%20%20%20%20%20Property%20'data'%20is%20missing%20in%20type%20'MyBadClass2'.%0D%0A*%2F%0D%0Aconst%20MyBadClass2%3A%20MyClass%20%3D%20class%20implements%20MyInterface%20%7B%0D%0A%20%20%20constructor(public%20values%3A%20number%5B%5D)%20%7B%7D%0D%0A%20%20%20static%20myStaticMethod()%3A%20string%20%7B%20return%20''%3B%20%7D%0D%0A%7D):

// dynamic part
interface MyInterface {
    data: number[];
}
// static part
interface MyStaticInterface {
    myStaticMethod(): string;
}

// type of a class that implements both static and dynamic interfaces
type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;

// way to make sure that given class implements both 
// static and dynamic interface
const MyClass: MyClass = class MyClass implements MyInterface {
   constructor(public data: number[]) {}
   static myStaticMethod(): string { return ''; }
}

// works just like a real class
const myInstance = new MyClass([]); // <-- works!
MyClass.myStaticMethod(); // <-- works!

// example of catching errors: bad static part
/*
type 'typeof MyBadClass1' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass1' is not assignable to type 'MyStaticInterface'.
    Property 'myStaticMethod' is missing in type 'typeof MyBadClass1'.
*/
const MyBadClass1: MyClass = class implements MyInterface {
   constructor(public data: number[]) {}
   static myNewStaticMethod(): string { return ''; }
}

// example of catching errors: bad dynamic part
/*
Type 'typeof MyBadClass2' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass2' is not assignable to type 'new (data: number[]) => MyInterface'.
    Type 'MyBadClass2' is not assignable to type 'MyInterface'.
      Property 'data' is missing in type 'MyBadClass2'.
*/
const MyBadClass2: MyClass = class implements MyInterface {
   constructor(public values: number[]) {}
   static myStaticMethod(): string { return ''; }
}
alexsapps commented 6 years ago

@aleksey-bykov this might not be Typescript's fault, but I couldn't get this working Angular component decorators and their AoT compiler.

bbugh commented 5 years ago

@aleksey-bykov that's clever but still doesn't work for abstract static. If you have any subclasses of MyClass, they are not enforced with type checking. It's also worse if you have generics involved.

// no errors
class Thing extends MyClass {

}

I really hope that the TypeScript team reconsiders their stance on this, because building end user libraries that require static attributes doesn't have any reasonable implementation. We should be able to have a contract that requires that interface implementers/abstract class extenders have statics.

zpdDG4gta8XKpMCd commented 5 years ago

@bbugh i question the very existence of the problem being discussed here, why would you need all these troubles with static abstract inherited methods if the same can be done via instances of regular classes?

class MyAbstractStaticClass {
    abstract static myStaticMethod(): void; // <-- wish we could
}
class MyStaticClass extends MyAbstractStaticClass {
    static myStaticMethod(): void {
         console.log('hi');
    }
}
MyStaticClass.myStaticMethod(); // <-- would be great

vs

class MyAbstractNonStaticClass {
    abstract myAbstractNonStaticMethod(): void;
}
class MyNonStaticClass extends MyAbstractNonStaticClass {
    myNonStaticMethod(): void {
        console.log('hi again');
    }
}
new MyNonStaticClass().myNonStaticMethod(); // <-- works today
Coder-256 commented 5 years ago

@aleksey-bykov There are plenty of reasons. For example (from @patryk-zielinski93):

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

I want to force implementation of static deserialize method in Serializable's subclasses. Is there any workaround to implement such behaviour?

EDIT: I know that you could also use deserialize as a constructor, but classes can only have 1 constructor, which makes factory methods necessary. People want a way to require factory methods in interfaces, which makes complete sense.

zpdDG4gta8XKpMCd commented 5 years ago

simply take desirealization logic to a separate class, because there is no benefit of having a static deserialize method attached to the very class being deserialized

class Deserializer {
     deserializeThis(...): Xxx {}
     deserializeThat(...): Yyy {}
}

what is the problem?

aigoncharov commented 5 years ago

@aleksey-bykov separate class doesn't look as pretty

octaharon commented 5 years ago

@aleksey-bykov, the problem is that there's more than 1 class requiring serialization, so your approach is forcing towards creating a dictionary of serializables, which is an uberclass antipattern, and every modification to any of them would require an edit in this uberclass, which makes code support a nuisance. While having a serializable interface could force an implementation to any given object type, and also support polymorphic inheritance, which are the main reasons people want it. Please do not speculate whether there's a benefit or not, anyone should be able to have options and to choose what is beneficial for his own project.

zpdDG4gta8XKpMCd commented 5 years ago

@octaharon having a class solely dedicated to deserialization is quite the opposite to what you said, it has single responsibility because the only time you change it is when you care about deserialization

contrary adding deserialization to the class itself like you proposed gives it an additional responsibility and additional reason for being changed

lastly i have no problem static methods , but i am truly curious to see an example of a practical use case for abstract static methods and static interfaces

octaharon commented 5 years ago

@octaharon having a class solely dedicated to deserialization is quite the opposite to what you said, it has single responsibility because the only time you change it is when you care about deserialization

except for you have to change the code in two places instead of one, as your (de)serialization depends on your type structure. That doesn't count towards "single responsibility"

contrary adding deserialization to the class itself like you proposed gives it an additional responsibility and additional reason for being changed

I don't get what you're saying. A class that is responsible for its own (de)serialization is exactly what I want, and please, don't tell me if it's right or wrong.

zpdDG4gta8XKpMCd commented 5 years ago

single responsibility doesn't say anything about how many files are involved, it only says there has to be a single reason

what i am saying is that when you add a new class you mean it for something other than just being deserialized, so it already has a reason to exist and responsibility assigned to it; then you add another responsibility of being able to deserialize itself, this gives us 2 responsibilities, this violates SOLID, if you keep adding more stuff into it like render, print, copy, transfer, encrypt, etc you gonna get a god class

and pardon me telling you banalities, but questions (and answers) about benefits and practical use cases is what drives this feature proposal to being implemented

octaharon commented 5 years ago

SOLID wasn't sent by God Allmighty on a tablet, and even if it was, there's a degree of freedom to its interpretation. You might be as idealistic as you wish about it, but do me a favor: don't spread your beliefs over the community. Everyone has all the rights to use any tool in any way he wants and break all possible rules he knows (you can't blame a knife for a murder). What defines the quality of a tool is a balance between a demand for certain features and an offer for them. And this branch shows the volume of the demand. If you don't need this feature - don't use it. I do. And a bunch of people here do need it too, and we have a practical use case, while you're telling that we should disregard it, in fact. It's only about what's more important for the maintainers - the holy principles (in whichever way they understand it), or the community.

zpdDG4gta8XKpMCd commented 5 years ago

man, any good proposal features use cases, this one doesn't

image

so i only expressed some curiosity and asked a question, since the proposal doesn't say it, why you people might need it

it boiled down to typical: just cause, don't you dare

personally i don't care about solid or oop, i just happened to overgrow it long time ago, you brought it up by throwing the "uberclass antipattern" argument and then backed up to "a degree of freedom to its interpretation"

the only practical reason mentioned in this whole discussion is this: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-308362119

A very common use case this will help with is React components, which are classes with static properties such as displayName, propTypes, and defaultProps.

and a few posts alike https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-345496014

but it's covered by (new (...) => MyClass) & MyStaticInterface

octaharon commented 5 years ago

that's the exact use case and a reason I'm here. Do you see the number of votes? Why do you think it's up to you personally to decide what's practical and what's not? Practical is what can be put to practice, and (at the moment of writing) 83 people would find this feature damn practical. Please respect others and read the full thread before you start pulling the phrases out of a context and exhibiting various buzzwords. Whatever you had overcome, that's definitely not your ego.

zpdDG4gta8XKpMCd commented 5 years ago

it's common sense that practical things are those that solve problems, non practical things are those to tingle your sense of beauty, i do respect others but with all that respect the question (mostly rhetorical now) still holds: what problem does this proposal intend to solve given (new (...) => MyClass) & MyStaticInterface for https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-308362119 and https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-289084844 just cause

please do not answer

ryuuji3 commented 5 years ago

For the same reason I sometimes use type declarations to reduce large annotations, I think a keyword like abstract static would be alot more readable than a relatively more difficult to digest construct like has been mentioned as an existing example.

Plus, we still haven't addressed abstract classes?

The solution to not use abstract classes is not the solution, in my opinion. That is a work-around! A work-around around what?

I think this feature request exists because many people, including the requester, have found that an expected feature such as abstract static or static in interfaces was not present.

Based on the solution offered, does the static keyword even have need to exist if there is a work-around to avoid its' use? I think that would be equally ridiculous to suggest.

The problem here is that static makes alot more sense. Based on the generated interest, can we have some less dismissive discussion?

ryuuji3 commented 5 years ago

Has there been any update on this proposal? Any arguments worth considering that demonstrate why we shouldn't have static abstract and the like? Can we have more suggestions that show why it would be useful?

Perhaps we need to reel things in and summarize what's been discussed and so we can figure out a solution.

There are two proposals as I understand:

  1. interfaces and types can define static properties and methods
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. abstract classes can define abstract static methods
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

class MyOtherClass extends MyClass<any> {
  static fromJson(json: string) {
  // unique implementation
  }
}

As for proposal one, there is technically a work-around! Which is not great, but that is something at least. But it IS a work-around.

You can split your interfaces into two and re-write your class like

interface StaticInterface {
  new(...args) => MyClass;
  fromJson(json): MyClass;
}

interface InstanceInterface {
  toJson(): string;
}

const MyClass: StaticInterface = class implements InstanceInterface {
   ...
}

In my opinion, this is alot of extra work and slightly less readable, and has the downside of re-writing your classes in a funny way which is simply strange and deviates from the syntax we are using.

But then, what about proposal 2? There is nothing that can be done about that, is there? I think that deserves addressing as well!

dsherret commented 5 years ago

What is the practical use for one of these types—how would one of these be used?

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

It's already possible to say a value must be an object like { fromJSON(serializedValue: string): JsonSerializable; }, so is this just wanted in order to enforce a pattern? I don't see the benefit to that from a type checking perspective. As a side note: in this case it would be enforcing a pattern that's hard to work with—it would be better to move the serialization process into separate serializer classes or functions for many reasons I won't get into here.

Additionally, why is something like this being done?

class FirstChildClass extends AbstractParentClass {
    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

What about using either template method or strategy pattern instead? That would work and be more flexible, right?

At the moment, I'm against this feature because to me it seems to add unnecessary complexity in order to describe a class designs that are hard to work with. Maybe there's some benefit I'm missing though?

zpdDG4gta8XKpMCd commented 5 years ago

there is one valid usecase for static React methods, thats about it

dsherret commented 5 years ago

@aleksey-bykov ah, ok. For those cases it might be better if adding a type on the constructor property would cause type checking in that rare scenario. For example:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

class MyComponent implements Component {
    static displayName = 5; // error
}

That seems way more worthwhile to me. It doesn't add any additional complexity to the language and only adds more work for the type checker when checking if a class implements an interface properly.


By the way, I was thinking a valid use case would be when traveling from an instance to the constructor and finally to a static method or property with type checking, but that's already possible by typing the constructor property on a type and will be solved for class instances in #3841.

zpdDG4gta8XKpMCd commented 5 years ago

i had a similar idea: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-437071092

thw0rted commented 5 years ago

Is the serialize/deserialize pattern described upthread not "valid"?

@dsherret doesn't "see the benefit from a type-checking perspective". The whole point of doing static analysis in the first place is to catch errors as early as possible. I type things so that if a call signature needs to change, then everybody who calls it -- or, critically, everybody responsible for implementing methods that use the signature -- update to the new signature.

Suppose I have a library that provides a set of sibling classes with a static foo(x: number, y: boolean, z: string) method, and the expectation is that users will write a factory class that takes several of these classes and calls the method to construct instances. (Maybe it's deserialize or clone or unpack or loadFromServer, doesn't matter.) The user also makes subclasses of the same (possibly abstract) parent class from the library.

Now, I need to change that contract so that the last parameter is an options object. Passing a string value for the 3rd argument is an unrecoverable error that should be flagged at compile-time. Any factory class should be updated to pass an object instead when calling foo, and subclasses that implement foo should change their call signature.

I want to guarantee that users who update to the new library version will catch the breaking changes at compile-time. The library has to export one of the static interface workarounds above (like type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;) and hope that the consumer applied it in all the right places. If they forgot to decorate one of their implementations or if they didn't use a library exported type to describe the call signature of foo in their factory class, the compiler can't tell anything changed and they get runtime errors. Contrast that with a sensible implementation of abstract static methods in the parent class -- no special annotations required, no burden on the consuming code, it just works out of the box.

dsherret commented 5 years ago

@thw0rted won't most libraries require some kind of registration of these classes and type checking can occur at that point? For example:

// in the library code...
class SomeLibraryContext {
    register(classCtor: Function & { deserialize(serializedString: string): Component; }) {
        // etc...
    }
}

// then in the user's code
class MyComponent extends Comonent {
    static deserialize(serializedString: string) {
        return JSON.parse(serializedString) as Component;
    }
}

const context = new SomeLibraryContext();
// type checking occurs here today. This ensures `MyComponent` has a static deserialize method
context.register(MyComponent);

Or are instances of these classes being used with the library and the library goes from the instance to the constructor in order to get the static methods? If so, it's possible to change the design of the library to not require static methods.

As a sidenote, as an implementer of an interface, I would be very annoyed if I was forced to write static methods because it's very difficult to inject dependencies to a static method because of the lack of a constructor (no ctor dependencies are possible). It also makes it difficult to swap out the implementation of the deserialization with something else depending on the situation. For example, say I was deserializing from different sources with different serialization mechanisms... now I have a static deserialize method which by design can only be implemented one way for an implementation of a class. In order to get around that, I would need to have another global static property or method on the class to tell the static deserialize method what to use. I'd prefer a separate Serializer interface which would allow for easily swapping out what to use and wouldn't couple the (de)serialization to the class being (de)serialized.

thw0rted commented 5 years ago

I see what you mean -- to use a factory pattern, eventually you have to pass the implementing class to the factory, and static checking occurs at that time. I still think there can be times when you want to provide a factory-compliant class but not actually use it at the moment, in which case an abstract static constraint would catch problems earlier and make meaning clearer.

I have another example that I think does not fall afoul of your "sidenote" concern. I have a number of sibling classes in a frontend project, where I give the user a choice of which "provider" to use for a certain feature. Of course, I could make dictionary somewhere of name => Provider and use the keys to determine what to show in the pick-list, but the way I have it implemented now is by requiring a static name field on each Provider implementation.

At some point, I changed that to requiring both a shortName and longName (for display in different contexts with different amounts of available screen space). It would have been much more straightforward to change abstract static name: string; to abstract static shortName: string; etc, instead of changing the pick-list component to have a providerList: Type<Provider> & { shortName: string } & { longName: string }. It conveys intent, in the right place (that is, in the abstract parent, not in the consuming component), and is easy to read and easy to change. I think we can say there's a workaround, but I still believe it's objectively worse than the proposed changes.

greeny commented 5 years ago

I recently stumbled upon this issue, when I needed to use static method in an interface. I apologize, if this has already been addressed before, as I do not have time to read through this amount of comments.

My current problem: I have a private .js library, that I want to use in my TypeScript project. So I went ahead and started writing a .d.ts file for that library, but as the library uses static methods, I could not really finish that. What is the suggested approach in this case?

Thanks for answers.

aigoncharov commented 5 years ago

@greeny https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-449610196

zpdDG4gta8XKpMCd commented 5 years ago

@greeny here is a working solution: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-437071092

specifically this part of it:

type MyClass = (new (text: string) => MyInterface) & { myStaticMethod(): string; }

simeyla commented 5 years ago

Another use-case : Generated code / partial class shims

interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

But I can't do this, and have to actually write a class - which is fine but still feels wrong. I just want - as many others have said - to say 'this is the contract'

Just wanted to add this here since I don't see any other mentions of code-generation - but many 'why would you need this' comments which just get irritating. I would expect a decent percentage of people 'needing' this 'static' feature in an interface are doing it just so they can refer to external library items.

Also I'd really like to see cleaner d.ts files - there must be some overlap with this feature and those files. It's hard to understand something like JQueryStatic because it seems like it's just a hack. Also the reality is d.ts files are often out of date and not maintained and you need to declare shims yourself.

(sorry for mentioning jQuery)

Nielio commented 5 years ago

For the serialization case i did something like that.

export abstract class World {

    protected constructor(json?: object) {
        if (json) {
            this.parseJson(json);
        }
    }

    /**
     * Apply data from a plain object to world. For example from a network request.
     * @param json Parsed json object
     */
    abstract parseJson(json: object): void;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

But it would be still way easier to use something like that:

export abstract class World {

    /**
     * Create a world from plain object. For example from a network request.
     * @param json Parsed json object
     */
    abstract static fromJson(json: object): World;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

I red a lot of this thread and i still don't understand why it is good and correct to say no to this pattern. If you share that opinion, you also say java and other popular langages make it wrong. Or am i wrong?

pavel-castornii commented 5 years ago

This issue is open two years and has 79 comments. It is labeled as Awaiting More Feedback. Could you say what else feedback you need to take a decision?

lebed2045 commented 5 years ago

it's really annoying that I can't describe static method neither in interface nor in abstract class (declaration only). It's so ugly to write workarounds because of this feature is yet to be implemented =(

fabb commented 5 years ago

For example, next.js uses a static getInitialProps function for getting page properties before constructing the page. In case it throws, the page does not get constructed, but rather the error page.

https://github.com/zeit/next.js/blob/master/packages/next/README.md#fetching-data-and-component-lifecycle

But unfortunately, clue ts which implement this method can give it any type signature, even if it causes errors at run time, because it cannot be type checked.

GongT commented 5 years ago

I think this issue exists here such long time is because JavaScript itself not good at static thing 🤔

Static inheritance should never exists in first place. 🤔🤔

Who want to call a static method or read a static field? 🤔🤔🤔

It's there any other use case?🤔🤔🤔🤔

brenfwd commented 5 years ago

Any update at all on this?

theseyi commented 5 years ago

If it's any help, what I've done in cases where I need to type the static interface for a class is use a decorator to enforce the static members on the class

The decorator is defined as:

export const statics = <T extends new (...args: Array<unknown>) => void>(): ((c: T) => void) => (_ctor: T): void => {};

If I have static constructor member interface defined as

interface MyStaticType {
  new (urn: string): MyAbstractClass;
  isMember: boolean;
}

and invoked on the class that should statically declare the members on T as:

@statics<MyStaticType>()
class MyClassWithStaticMembers extends MyAbstractClass {
  static isMember: boolean = true;
  // ...
}
GerkinDev commented 5 years ago

The most frequent example is good:

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

But as said in #13462 here:

Interfaces should define the functionality an object provides. This functionality should be overridable and interchangeable (that's why interface methods are virtual). Statics are a parallel concept to dynamic behaviour/virtual methods.

I agree on the point that interfaces, in TypeScript, describe just an object instance itself, and how to use it. The problem is that an object instance is not a class definition, and a static symbol may only exists on a class definition.

So I may propose the following, with all its flaws:


An interface could be either describing an object, or a class. Let's say that a class interface is noted with the keywords class_interface.

class_interface ISerDes {
    serialize(): string;
    static deserialize(str: string): ISerDes
}

Classes (and class interfaces) could use the statically implements keywords to declare their static symbols using an object interface (class interfaces could not be statically implemented).

Classes (and class interfaces) would still use the implements keyword with an object interface or a class interface.

A class interface could then the mix between a statically implemented object interface and an instance implemented interface. Thus, we could get the following:

interface ISerializable{
    serialize(): string;
}
interface IDeserializable{
    deserialize(str: string): ISerializable
}

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

This way, interfaces could keep their meaning, and class_interface would be a new kind of abstraction symbol dedicated for classes definitions.

yadimon commented 5 years ago

One small not-critical use-case more: In Angular for AOT compiling, you can not call functions in decorators (like in @NgModule module decorator) angular issue

For service worker module, you need thing like this: ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}) Our environment using subclasses, extending abstract class with default values and abstract properties to implement. Similar implementation example So AOT does not work because construct a class is a function, and throws error like: Function calls are not supported in decorators but ..

To make it work and keep autocompletion/compiler support, its possible to define same properties on static level. But for 'to implement' properties, we need interface with static members or abstract static members in abstract class. Both not possible yet.

with interface could work this way:

// default.env.ts
interface ImplementThis {
  static propToImplement: boolean;
}

class DefaultEnv {
  public static production: boolean = false;
}

// my.env.ts
class Env extends DefaultEnv implements ImplementThis {
  public static propToImplement: true;
}

export const environment = Env;

with abstract statics could work this way:

// default.env.ts
export abstract class AbstractDefaultEnv {
  public static production: boolean = false;
  public abstract static propToImplement: boolean;
}
// my.env.ts
class Env extends AbstractDefaultEnv {
  public static propToImplement: true;
}

export const environment = Env;

there are workarounds, but all they are weak :/

brenfwd commented 5 years ago

@DanielRosenwasser @RyanCavanaugh apologies for the mentions, but it seems that this feature suggestion—which has a lot of support from the community, and I feel would be fairly easy to implement—has gotten buried deep in the Issues category. Do either of you have any comments about this feature, and would a PR be welcome?

dsherret commented 5 years ago

Isn't this issue a duplicate of #1263? 😛

26398 (Type check static members based on implements type's constructor property) looks like a better solution... if something like this were to be implemented then I'd hope it's that one. That doesn't require any additional syntax/parsing and is only a type checking change for a single scenario. It also doesn't raise as many questions as this does.

brenfwd commented 5 years ago

I feel as though static methods in interfaces is not as intuitive as having static abstract methods on abstract classes.

I think it is a little sketchy to add static methods to interfaces because an interface should define an object, not a class. On the other hand, an abstract class should certainly be allowed to have static abstract methods, since abstract classes are used for defining subclasses. As far as implementation of this goes, it would simply just need to be type-checked when extending the abstract class (e.g. class extends MyAbstractClass), not when using it as a type (e.g. let myInstance: MyAbstractClass).

Example:

abstract class MyAbstractClass {
  static abstract bar(): number;
}

class Foo extends MyAbstractClass {
  static bar() {
    return 42;
  }
}
lebed2045 commented 5 years ago

right now out of necessity I use this

abstract class MultiWalletInterface {

  static getInstance() {} // can't write a return type MultiWalletInterface

  static deleteInstance() {}

  /**
   * Returns new random  12 words mnemonic seed phrase
   */
  static generateMnemonic(): string {
    return generateMnemonic();
  }
}

this is inconvenient!

Xample commented 5 years ago

I came with a problem where I am adding properties to the "Object", here is a sandbox example

interface Object {
    getInstanceId: (object: any) => number;
}

Object.getInstanceId = () => 42;
const someObject = {};
Object.getInstanceId(someObject); // correct
someObject.getInstanceId({}); // should raise an error but does not

Any object instance are now considered to have the property getInstanceId while only the Object should. With a static property, the problem would have been solved.

thw0rted commented 5 years ago

You want to augment ObjectConstructor, not Object. You're declaring an instance method, when you actually want to attach a method to the constructor itself. I think this is already possible through declaration merging:

declare global {
    interface ObjectConstructor {
        hello(): string;
    }
}

Object.hello();
Xample commented 5 years ago

@thw0rted Excellent! thank you I wasn't aware of ObjectConstructor

thw0rted commented 5 years ago

The larger point is that you're augmenting the constructor-type rather than the instance-type. I just looked up the Object declaration in lib.es5.d.ts and found that it's of type ObjectConstructor.

patricio-ezequiel-hondagneu-roig commented 5 years ago

It's hard for me to believe this issue is still around. This is a legitimately useful feature, with several actual use cases.

The whole point of TypeScript is to be able to ensure type safety in our codebase, so, why is this feature still "pending for feedback" after two years worth of feedback?

acewert commented 5 years ago

I may be way off on this, but would something like Python's metaclasses be able to solve this problem in a native, sanctioned way (i.e. not a hack or workaround) without violating the paradigm of TypeScript (i.e. keeping separate TypeScript types for the instance and the class)?

Something like this:

interface DeserializingClass<T> {
    fromJson(serializedValue: string): T;
}

interface Serializable {
    toJson(): string;
}

class Foo implements Serializable metaclass DeserializingClass<Foo> {
    static fromJson(serializedValue: string): Foo {
        // ...
    }

    toJson(): string {
        // ...
    }
}

// And an example of how this might be used:
function saveObject(Serializable obj): void {
    const serialized: string = obj.toJson();
    writeToDatabase(serialized);
}

function retrieveObject<T metaclass DeserializingClass<T>>(): T {
    const serialized: string = getFromDatabase();
    return T.fromJson(serialized);
}

const foo: Foo = new Foo();
saveObject(foo);

const bar: Foo = retrieveObject<Foo>();

Honestly the trickiest part of this approach seems like it would be coming up with a meaningful, TypeScript-y keyword for metaclass... staticimplements, classimplements, withstatic, implementsstatic... not sure.

This is a bit like @GerkinDev's proposal but without the separate kind of interface. Here, there is a single concept of interface, and they can be used to describe the shape of an instance or of a class. Keywords in the implementing class definition would tell the compiler which side each interface should be checked against.

RyanCavanaugh commented 5 years ago

Let's pick up the discussion at #34516 and #33892 depending on which feature you're going for