Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
vyshkant opened this issue Mar 11, 2017 · 98 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@vyshkant
Copy link

vyshkant commented Mar 11, 2017

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

  • The declaration of an abstract static method of an abstract class should be marked with abstract static modifier
  • The implementation of abstract static method of an abstract class shoul be marked with abstract static or static modifier
  • The declaration of static method of an interface should be marked with static modifier
  • The implementation of static method of an interface shoul be marked with static modifier

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

  • Syntactic
    • What is the grammar of this feature? - The grammar of this feature is abstract static modifier of an abstract class method and static modifier of an interface method.
    • Are there any implications for JavaScript back-compat? If so, are they sufficiently mitigated? - There is no any implications for JavaScript back-compat.
    • Does this syntax interfere with ES6 or plausible ES7 changes? - This syntax does not interfere with ES6 or plausible ES7 changes.
  • Semantic
    • What is an error under the proposed feature? - Now there are errors compiling abstract static modifier of an abstract class method and static modifier of an interface method. Proposed feature have to fix these errors.
    • How does the feature impact subtype, supertype, identity, and assignability relationships? - The feature does not impact subtype, supertype, identity, and assignability relationships.
    • How does the feature interact with generics? - The feature does not interact with generics.
  • Emit
    • What are the effects of this feature on JavaScript emit? - There are no effects of this feature on JavaScript emit.
    • Does this emit correctly in the presence of variables of type ‘any’? - Yes.
    • What are the impacts to declaration file (.d.ts) emit? - There is no impacts to declaration file.
    • Does this feature play well with external modules? - Yes.
  • Compatibility
    • Is this a breaking change from the 1.0 compiler? - Probably yes, 1.0 compiler will not be able to compile the code implementing this feature.
    • Is this a breaking change from JavaScript behavior? - No.
    • Is this an incompatible implementation of a future JavaScript (i.e. ES6/ES7/later) feature? - No.
  • Other
    • Can the feature be implemented without negatively affecting compiler performance? - Probably yes.
    • What impact does it have on tooling scenarios, such as member completion and signature help in editors? - Probably it does not have any impact of this type.
@RyanCavanaugh
Copy link
Member

Static interface methods generally don't make sense, see #13462

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this In Discussion Not yet reached consensus Suggestion An idea for TypeScript and removed Help Wanted You can do this labels Mar 13, 2017
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed In Discussion Not yet reached consensus labels Mar 14, 2017
@RyanCavanaugh
Copy link
Member

Holding off on this until we hear more feedback on it.

A major question we had when considering this: Who is allowed to call an abstract static method? Presumably you can't invoke AbstractParentClass.getSomeClassDependentValue directly. But can you invoke the method on an expression of type AbstractParentClass? If so, why should that be allowed? If not, what is the use of the feature?

@vyshkant
Copy link
Author

vyshkant commented Mar 15, 2017

Static interface methods generally don't make sense, see #13462

In the discussion of #13462 I didn't see why static interface methods are senseless. I only saw that their functionality can be implemented by other means (which proves that they are not senseless).

From a practical point of view, the interface is a kind of specification - a certain set of methods that are mandatory for their implementation in a class that implements this interface. The interface doesn't only define the functionality an object provides, it is also a kind of contract.

So I don't see any logical reasons why class may have static method and interface doesn't.

If we follow the point of view that everything that can already be implemented in the language does not need any improvements of syntax and other things (namely, this argument was one of the main points in the discussion of #13462), then guided by this point of view, we can decide that the while cycle is redundant because it can be implemented using for and if together. But we are not going to do away with while.

@vyshkant
Copy link
Author

vyshkant commented Mar 15, 2017

A major question we had when considering this: Who is allowed to call an abstract static method? Presumably you can't invoke AbstractParentClass.getSomeClassDependentValue directly. But can you invoke the method on an expression of type AbstractParentClass? If so, why should that be allowed? If not, what is the use of the feature?

Good question. Since you were considering this issue, could you please share your ideas about this?

It only comes to my mind that on the compiler level the case of direct call of AbstractParentClass.getSomeClassDependentValue will not be tracked (because it cannot be tracked), and the JS runtime error will occur. But I'm not sure whether this is consistent with the TypeScript ideology.

@kitsonk
Copy link
Contributor

kitsonk commented Mar 15, 2017

I only saw that their functionality can be implemented by other means (which proves that they are not senseless).

Just because something is implementable, doesn't mean it makes sense. 😉

@patryk-zielinski93
Copy link

patryk-zielinski93 commented Mar 24, 2017

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?

@intoxopox
Copy link

intoxopox commented May 9, 2017

What's the latest take on this? I just tried to write an abstract static property of an abstract class and was genuinely surprised when it wasn't allowed.

@jimmykane
Copy link

jimmykane commented May 30, 2017

What @patryk-zielinski93 said. Coming from some years of projects in PHP that we do convert to TS we DO want static methods in interfaces.

@OliverJAsh
Copy link
Contributor

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

Because of this limitation, the typings for React currently include two types: a Component class, and ComponentClass interface including the constructor function and static properties.

To fully type check a React component with all its static properties, one has two use both types.

Example without ComponentClass: static properties are ignored

import React, { Component, ComponentClass } from 'react';

type Props = { name: string };

{
  class ReactComponent extends Component<Props, any> {
    // expected error, but got none: displayName should be a string
    static displayName = 1
    // expected error, but got none: defaultProps.name should be a string
    static defaultProps = { name: 1 }
  };
}

Example with ComponentClass: static properties are type checked

{
  // error: displayName should be a string
  // error: defaultProps.name should be a string
  const ReactComponent: ComponentClass<Props> = class extends Component<Props, any> {
    static displayName = 1
    static defaultProps = { name: 1 }
  };
}

I suspect many people are currently not using ComponentClass, unaware that their static properties are not being type checked.

Related issue: DefinitelyTyped/DefinitelyTyped#16967

@roboslone
Copy link

Is there any progress on this? Or any workaround for constructors on abstract classes?
Here's an example:

abstract class Model {
    abstract static fromString(value: string): Model
}

class Animal extends Model {
    constructor(public name: string, public weight: number) {}

    static fromString(value: string): Animal {
        return new Animal(...JSON.parse(value))
    }
}

@ghost
Copy link

ghost commented Aug 15, 2017

@roboslone

class Animal {
    static fromString(value: string): Animal {
        return new Animal();
    }
}

function useModel<T>(model: { fromString(value: string): T }): T {
    return model.fromString("");
}

useModel(Animal); // Works!

@malina-kirn
Copy link

Agreed that this is an extremely powerful and useful feature. In my view, this feature is what makes classes 'first class citizens'. Inheritance of class/static methods can and does make sense, particularly for the static method factory pattern, which has been called out here by other posters several times. This pattern is particularly useful for deserialization, which is a frequently performed operation in TypeScript. For instance, it makes perfect sense to want to define an interface that provides a contract stating that all implementing types are instantiable from JSON.

Not allowing abstract static factory methods requires the implementor to create abstract factory classes instead, unnecessarily doubling the number of class definitions. And, as other posters have pointed out, this is a powerful and successful feature implemented in other languages, such as PHP and Python.

@jasonayre
Copy link

New to Typescript, but I am also surprised this isn't allowed by default, and that so many people are trying to justify not adding the feature with either:

  1. TS doesn't need the feature, because you can still accomplish what you are trying to do through other means (which is only a valid argument if you provide an example of a very objectively better way of doing something, of which I've seen very little)
  2. Just because we can doesn't mean we should. Great: but people are posting specific examples of how it would be useful/beneficial. I don't see how it could hurt to allow it.

Another simple use case: (ideal way, which does not work)

import {map} from 'lodash';

export abstract class BaseListModel {
  abstract static get instance_type();
  
  items: any[];

  constructor(items?: any[]) {
    this.items = map(items, (item) => { return new this.constructor.instance_type(item) });
  }

  get length() { return this.items.length; }
}

export class QuestionList extends BaseListModel {
  static get instance_type() { return Question }
}

Instead, the list instance ends up exposing the instance type directly, which is not relevant to the list instance itself and should be accessed through the constructor. Feels dirty. Would feel much dirtier if we were talking about a record model, in which specify a set of default values via the same sort of mechanism, etc.

I was really excited to use real abstract classes in a language after being in ruby/javascript for so long, only to end up dismayed by the implementation restrictions - The above example was just my first example of encountering it, though I can think of many other use cases where it would be useful. Mainly, just as a means of creating simple DSL's/configuration as part of the static interface, by ensuring that a class specifies a default values object or whatever it may be. - And you may be thinking, well that's what interfaces are for. But for something simple like this, it doesn't really make sense, and only ends up in things being more complicated than they need to be (the subclass would need to extend the abstract class and implement some interface, makes naming things more complicated, etc).

@ZheyangSong
Copy link

ZheyangSong commented Sep 29, 2017

I had this similar requirement for my project two times. Both of them were related to guarantee that all subclasses provide concrete implementations of a set of static methods. My scenario is described below:

class Action {
  constructor(public type='') {}
}

class AddAppleAction extends Action {
  static create(apple: Apple) {
    return new this(apple);
  }
  constructor(public apple: Apple) {
    super('add/apple');
  }
}

class AddPearAction extends Action {
  static create(pear: Pear) {
    return new this(pear);
  }

  constructor(public pear: Pear) {
    super('add/pear');
  }
}

const ActionCreators = {
  addApple: AddAppleAction
  addPear: AddPearAction
};

const getActionCreators = <T extends Action>(map: { [key: string]: T }) => {
  return Object.entries(map).reduce((creators, [key, ActionClass]) => ({
    ...creators,
    // To have this function run properly,
    // I need to guarantee that each ActionClass (subclass of Action) has a static create method defined.
    // This is where I want to use abstract class or interface to enforce this logic.
    // I have some work around to achieve this by using interface and class decorator, but it feels tricky and buggy. A native support for abstract class method would be really helpful here.
    [key]: ActionClass.create.bind(ActionClass)
  }), {});
};

Hope this can explain my requirements. Thanks.

@pathurs
Copy link
Contributor

pathurs commented Oct 1, 2017

For anyone looking for a workaround, you can use this decorator:

class myClass {
    public classProp: string;
}

interface myConstructor {
    new(): myClass;
    
    public readonly staticProp: string;
}

function StaticImplements <T>() {
    return (constructor: T) => { };
}

@StaticImplements <myConstructor>()
class myClass implements myClass {}

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Oct 2, 2017

const getActionCreators = <T extends Action>(map: { [key: string]: {new () => T, create() : T}}) => {

since we're calling through a type parameter, the actual base class with its hypothetical abstract static factory method will never be involved. The types of the subclasses are related structurally when the type parameter is instantiated.

What I'm not seeing in this discussion are cases where the implementation is invoked by way of the type of the base class as opposed to some synthesized type.

It is also relevant to consider that, unlike in languages such as C# where abstract members are actually overridden by their implementations in deriving classes, JavaScript member implementations, instance or otherwise, never override inherited numbers, but rather shadow them.

@davidmpaz
Copy link

My 5 cents here, from a user perspective point of view is:

For the interfaces case, it should be allowed the static modifier

Interfaces define contracts, to be fulfilled by implementing classes. This mean that interfaces are abstractions on a higher level than classes.

Right now what I can see is that classes can be more expressive than interfaces, in a way that we can have static method in classes but we can not enforce that, from the contract definition itself.

In my opinion that feels wrong and hindering.

Is there any technical background making this language feature hard or impossible to implement?

cheers

@kitsonk
Copy link
Contributor

kitsonk commented Oct 24, 2017

Interfaces define contracts, to be fulfilled by implementing classes. This mean that interfaces are abstractions on a higher level than classes.

Classes have two interfaces, two implementation contracts, and there is no getting away from that.
There are reasons why languages like C# don't have static members to interfaces as well. Logically, interfaces are the public surface of an object. An interface describes the public surface of an object. That means it doesn't contain anything that isn't there. On instances of classes, static methods are not present on the instance. They only exist on the class/constructor function, therefore they should only be described in that interface.

@simeyla
Copy link

simeyla commented Mar 7, 2019

Another use-case : Generated code / partial class shims

  • I'm using the @rsuter/nswag library, which generates swagger specifications.
  • You can write an 'extensions' file that gets merged into the generated file.
  • The extensions file is never run, but it needs to compile by itself!
  • Sometimes within that file I need to refer to something which doesn't yet exist (because it's generated)
  • Therefore I want to declare a shim / interface for it as follows
  • Note: You can specify that certain import statements are ignored in the final merging so the 'shim' include is ignored in the final generated code.
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
Copy link

Nielio commented Apr 18, 2019

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
Copy link

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
Copy link

lebed2045 commented Apr 27, 2019

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
Copy link

fabb commented Apr 28, 2019

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
Copy link

GongT commented Apr 30, 2019

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? 🤔🤔🤔

  • child class: It must not static
  • parent class: use constructor argument
  • other: use ISomeClassConstructor interface

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

@brenfwd
Copy link

brenfwd commented May 2, 2019

Any update at all on this?

@theseyi
Copy link

theseyi commented May 2, 2019

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
Copy link

GerkinDev commented May 2, 2019

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
Copy link

yadimon commented May 28, 2019

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
Copy link

brenfwd commented May 29, 2019

@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
Copy link
Contributor

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
Copy link

brenfwd commented May 29, 2019

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
Copy link

lebed2045 commented Jun 22, 2019

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
Copy link

Xample commented Sep 20, 2019

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
Copy link

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
Copy link

Xample commented Sep 20, 2019

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

@thw0rted
Copy link

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

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
Copy link

acewert commented Oct 15, 2019

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
Copy link
Member

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

@microsoft microsoft locked and limited conversation to collaborators Oct 16, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests