-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Static interface methods generally don't make sense, see #13462 |
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 |
In the discussion of #13462 I didn't see why 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 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 |
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 |
Just because something is implementable, doesn't mean it makes sense. 😉 |
I want to force implementation of static deserialize method in Serializable's subclasses. |
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. |
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. |
A very common use case this will help with is React components, which are classes with static properties such as Because of this limitation, the typings for React currently include two types: a To fully type check a React component with all its static properties, one has two use both types. Example without 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 {
// 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 Related issue: DefinitelyTyped/DefinitelyTyped#16967 |
Is there any progress on this? Or any workaround for constructors on abstract classes? 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))
}
} |
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! |
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. |
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:
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). |
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. |
For anyone looking for a workaround, you can use this decorator:
|
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 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 |
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 |
Classes have two interfaces, two implementation contracts, and there is no getting away from that. |
Another use-case : Generated code / partial class shims
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 (sorry for mentioning jQuery) |
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? |
This issue is open two years and has 79 comments. It is labeled as |
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 =( |
For example, next.js uses a static 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. |
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?🤔🤔🤔🤔 |
Any update at all on this? |
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;
// ...
} |
The most frequent example is good: interface JsonSerializable {
toJSON(): string;
static fromJSON(serializedValue: string): JsonSerializable;
}
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 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 ISerDes {
serialize(): string;
static deserialize(str: string): ISerDes
} Classes (and class interfaces) could use the Classes (and class interfaces) would still use the 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 |
One small not-critical use-case more: For service worker module, you need thing like this: 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:
with abstract statics could work this way:
there are workarounds, but all they are weak :/ |
@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? |
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. |
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. Example: abstract class MyAbstractClass {
static abstract bar(): number;
}
class Foo extends MyAbstractClass {
static bar() {
return 42;
}
} |
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! |
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 |
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(); |
@thw0rted Excellent! thank you I wasn't aware of ObjectConstructor |
The larger point is that you're augmenting the constructor-type rather than the instance-type. I just looked up the |
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? |
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 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. |
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 allowingabstract 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):
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):
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):
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 thestatic
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:
For example, this TypeScript code (example 4):
should be compiled to this JS code:
4. Relevant points
abstract static
modifierabstract static
orstatic
modifierstatic
modifierstatic
modifierAll the other properties of
abstract static
modifier should be inherited fromabstract
andstatic
modifiers properties.All the other properties of
static
interface method modifier should be inherited from the interface methods andstatic
modifier properties.5. Language Feature Checklist
abstract static
modifier of an abstract class method andstatic
modifier of an interface method.abstract static
modifier of an abstract class method andstatic
modifier of an interface method. Proposed feature have to fix these errors.The text was updated successfully, but these errors were encountered: