Closed vyshkant closed 5 years ago
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 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?
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
.
A major question we had when considering this: Who is allowed to call an
abstract static
method? Presumably you can't invokeAbstractParentClass.getSomeClassDependentValue
directly. But can you invoke the method on an expression of typeAbstractParentClass
? 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.
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. 😉
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?
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 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: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967
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))
}
}
@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!
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:
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 {}
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.
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
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.
On instances of classes, static methods are not present on the instance.
can you elaborate on that? This is not C#
They only exist on the class/constructor function, therefore they should only be described in that interface.
That we got it and it's what we want to change.
Interfaces define contracts, to be fulfilled by implementing classes. This mean that interfaces are abstractions on a higher level than classes.
I fully agree on that. See the Json interface example
Hi @kitsonk, please could you elaborate more on:
Classes have two interfaces, two implementation contracts, and there is no getting away from that.
I did not understand that part.
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.
I agree. I don't see any contradiction to what I said. I even said more. I said an interface is a contract for a class to be fulfilled.
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.
Not sure I understood this right, it is clear what it say, but not why you say it. I can explain why I think is still valid my statement. I am passing along interfaces as parameters all the time to my methods, this mean I have access to interface methods, please note that I am not using interfaces here as a way to define data structure but to define concrete objects that get created/hydrated elsewhere. So when I have:
fetchData(account: SalesRepresentativeInterface): Observable<Array<AccountModel>> {
// Method Body
}
In that method body, I can indeed use account
methods. What I would like to be possible to do is to be able to use static methods from SalesRepresentativeInterface
which were enforced already to be implemented in whatever the class I am receiving in account
. Maybe I am having a very simplistic or completely wrong idea on how to use the feature.
I think that allowing the static
modifier will allow me to do something like: SalesRepresentativeInterface.staticMethodCall()
Am I wrong ?
cheers
@davidmpaz : your syntax isn't quite right, but close. Here's an example usage pattern:
interface JSONSerializable {
static fromJSON(json: any): JSONSerializable;
toJSON(): any;
}
function makeInstance<T extends JSONSerializable>(cls: typeof T): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializable implements JSONSerializable {
constructor(private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializable {
return new ImplementsJSONSerializable(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);
Unfortunately, for this to work, we need two features from TypeScript: (1) static methods in interfaces and abstract static methods; (2) the ability to use typeof
as a type hint with generic classes.
@davidmpaz
I did not understand that part.
Classes have two interfaces. The constructor function and the instance prototype. The keyword class
essentially is sugar for that.
interface Foo {
bar(): void;
}
interface FooConstructor {
new (): Foo;
prototype: Foo;
baz(): void;
}
declare const Foo: FooConstructor;
const foo = new Foo();
foo.bar(); // instance method
Foo.baz(); // static method
@jimmykane
can you elaborate on that? This is not C#
class Foo {
bar() {}
static baz() {}
}
const foo = new Foo();
foo.bar();
Foo.baz();
There is no .baz()
on instances of Foo
. .baz()
only exists on the constructor.
From a type perspective, you can reference/extract this two interfaces. Foo
refers to the public instance interface and typeof Foo
refers to the public constructor interface (which would include the static methods).
To follow up on @kitsonk's explanation, here is the example above rewritten to achieve what you want:
interface JSONSerializable <T>{
fromJSON(json: any): T;
}
function makeInstance<T>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializable {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializable {
return new ImplementsJSONSerializable(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);
Note here that:
implements
clause is not really needed, every time you use the class, a structural check is done and the class will be validated to match the interface needed. typeof
to get the constructor type, just make sure your T
is what you intend to capture@mhegazy : you've had to remove my toJSON
call in the JSONSerializable interface. Although my simple example function makeInstance
only calls fromJSON
, it's very common to want to instantiate an object, then use it. You've removed my ability to make calls on what's returned by makeInstance
, because I don't actually know what T
is inside makeInstance
(particularly relevant if makeInstance
is a class method used internally to create an instance, then use it). Of course I could do this:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable <T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer implements JSONSerializer {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);
And now I know that my T
will have all the methods available on JSONSerializer
. But this overly verbose and difficult to reason about (wait, you're passing ImplementsJSONSerializer
, but this isn't a JSONSerializable
, is it? Wait, you're duck-typing??). The easier-to-reason about version is:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable <T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer implements JSONSerializer {
constructor (private json: any) {
}
toJSON(): any {
return this.json;
}
}
class ImplementsJSONSerializable implements JSONSerializable<ImplementsJSONSerializer> {
fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(new ImplementsJSONSerializable());
But as I pointed out in a previous comment, we now must create a factory class for every class we want to instantiate, which is even more verbose than the duck-typing example. That's certainly a workable pattern done in Java all the time (and I assume C# as well). But it's unnecessarily verbose and duplicative. All that boilerplate goes away if static methods are allowed in interfaces and abstract static methods are allowed in abstract classes.
no need for the additional class. classes can have static
members.. you just do not need the implements
clause. in a nominal type system you really need it to assert the "is a" relationship on your class. in a structural type system, you do not really need that. the "is a" relationship is checked on every use anyways, so you can safely drop the implements
clause and not loose safety.
so in other words, you can have one class that has a static fromJSON
and whose instances has a toJSON
:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable<T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);
@mhegazy I already pointed that out as a viable option. See my first 'duck-typing' example. I argued that it is unnecessarily verbose and difficult to reason about. I then asserted that the version that is easier to reason about (factory classes) is even more verbose.
No-one disagrees that it's possible to work around the lack of static methods in interfaces. In fact, I'd argue there's a more elegant workaround by using functional styles over factory classes, though that's my personal preference.
But, in my view, the cleanest and easiest-to-reason about way to implement this exceptionally common pattern is using abstract static methods. Those of us who have come to love this feature in other languages (Python, PHP) miss having it in TypeScript. Thanks for your time.
I then asserted that the version that is easier to reason about (factory classes)
not sure i agree that this is easier to reason about.
But, in my view, the cleanest and easiest-to-reason about way to implement this exceptionally common pattern is using abstract static methods. Those of us who have come to love this feature in other languages (Python, PHP) miss having it in TypeScript.
And this issue is tracking getting this added. I just wanted to make sure future visitors to this thread understand that this is doable today without an implements
clause.
@kitsonk right I missed that.
Guys, so I have like LocationModule, which should have method for getting options to store in database, from which it should recreate itself. Every LocationModule have their own options with type for recreation.
So now I have to create factory with generics for recreation and even then I do not have compile time checks. You know, beating the purpose here.
At first I was like "well no static for interfaces so let's stick with abstract class BUT even this would be dirty, since now I have to change everywhere in my code type from interface to abstract class so checker would recognize methods I'm looking for.
Here:
export interface LocationModuleInterface {
readonly name: string;
getRecreationOptions(): ModuleRecreationOptions;
}
export abstract class AbstractLocationModule implements LocationModuleInterface {
abstract readonly name: string;
abstract getRecreationOptions(): ModuleRecreationOptions;
abstract static recreateFromOptions(options: ModuleRecreationOptions): AbstractLocationModule;
}
And then I stumble upon well, static cannot be with abstract. And it cannot be in interface.
Guys, seriously, aren't we protecting not implementing this only for not implementing this?
I love TypeScript passionately. Like crazy I mean. But there is always but.
Instead of having forced implementation of static method I would have to double check like I was doing with everything in plain old JavaScript.
Architecture is already full of patterns, cool decisions etc, but some of them are just to overcome issues with using such simple things like interface static method.
My factory will have to check at runtime static method existence. How is that?
Or even better:
export interface LocationModuleInterface {
readonly name: string;
getRecreationOptions(): ModuleRecreationOptions;
dontForgetToHaveStaticMethodForRecreation();
}
If you're using the class object polymorphically, which is the only reason for it to implement an interface, it does not matter if the interface(s)that specifies the requirements for the static side of the class is syntactically referenced by the class itself because it will be checked at the use site and issue a design time error if the requisite interface(is) is not implemented.
@malina-kirn
See my first 'duck-typing' example.
This issue has no bearing on whether or not duck typing is used. TypeScript is duck typed.
@aluanhaddad Your implication is not correct. I can understand your point that implements ISomeInterface
primary function is for using the class object polymorphically. However it CAN matter whether the class references the interface(s) that describe the class object's static shape.
You imply that implicit type inference and usage site errors cover all use cases. Consider this example:
interface IComponent<TProps> {
render(): JSX.Element;
}
type ComponentStaticDisplayName = 'One' | 'Two' | 'Three' | 'Four';
interface IComponentStatic<TProps> {
defaultProps?: Partial<TProps>;
displayName: ComponentStaticDisplayName;
hasBeenValidated: 'Yes' | 'No';
isFlammable: 'Yes' | 'No';
isRadioactive: 'Yes' | 'No';
willGoBoom: 'Yes' | 'No';
}
interface IComponentClass<TProps> extends IComponentStatic<TProps> {
new (): IComponent<TProps> & IComponentStatic<TProps>;
}
function validateComponentStaticAtRuntime<TProps>(ComponentStatic: IComponentStatic<TProps>, values: any): void {
if(ComponentStatic.isFlammable === 'Yes') {
// something
} else if(ComponentStatic.isFlammable === 'No') {
// something else
}
}
// This works, we've explicitly described the object using an interface
const componentStaticInstance: IComponentStatic<any> = {
displayName: 'One',
hasBeenValidated: 'No',
isFlammable: 'Yes',
isRadioactive: 'No',
willGoBoom: 'Yes'
};
// Also works
validateComponentStaticAtRuntime(componentStaticInstance, {});
class ExampleComponent1 implements IComponent<any> {
public render(): JSX.Element { return null; }
public static displayName = 'One'; // inferred as type string
public static hasBeenValidated = 'No'; // ditto ...
public static isFlammable = 'Yes';
public static isRadioactive = 'No';
public static willGoBoom = 'Yes';
}
// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent1, {});
class ExampleComponent2 implements IComponent<any> {
public render(): JSX.Element { return null; }
public static displayName = 'One';
public static hasBeenValidated = 'No';
public static isFlammable = 'Yes';
public static isRadioactive = 'No';
public static willGoBoom = 'Yes';
}
// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent2, {});
In the above example explicit type declaration is required; not describing the shape of the class object (static side) results in a design time error at the usage site even though the actual values conform to the expected shape.
Furthermore, the lack of a way to reference the interface(s) describing the static side, leave us with the only option being to explicitly declare the type on each individual member; and then to repeat that on every class.
Building on my previous post, I feel that the static
modifier in interfaces is a really good solution to some use cases that need to be resolved. I apologize for how big this comment has grown but there is a lot that I want to illustrate.
1) There are times when we need to be able to explicitly describe the shape of the static side of a class object, such as in my example above. 2) There are times when shape checking at the definition site is highly desirable, and times such as in @OliverJAsh 's example where the use site is in external code that won't be checked and you need to shape check at the definition site.
In regards to number 2, many posts I've read suggest shape checking as the use site is more than sufficient. But in cases where the use site is in a galaxy module far, far away.... or when the use site is in an external location that won't be checked this is obviously not the case.
Other posts suggest #workarounds
to shape check at the definition site. While these dreaded workarounds will allow you to check the shape at the definition site (in some cases), there are problems:
#workaroundsToMakeTheWorkaroundsWork
is not fun... or productive... but mostly not fun.Here is an example of the workaround I've seen lately to force shape checking at the definition site...
// (at least) two separate interfaces
interface ISomeComponent { ... }
interface ISomeComponentClass { ... }
// confusing workaround with extra work to use, not very intuitive, etc
export const SomeComponent: ISomeComponentClass =
class SomeComponent extends Component<IProps, IState> implements ISomeComponent {
...
}
Now try using that workaround with an abstract class thrown in
interface ISomeComponent { ... }
// need to separate the static type from the class type, because the abstract class won't satisfy
// the shape check for new(): ...
interface ISomeComponentStatic { ... }
interface ISomeComponentClass {
new (): ISomeComponent & ISomeComponentStatic;
}
// I AM ERROR. ... cannot find name abstract
export const SomeComponentBase: ISomeComponentStatic =
abstract class SomeComponentBase extends Component<IProps, IState> implements ISomeComponent {
...
}
export const SomeComponent: ISomeComponentClass =
class extends SomeComponentBase { ... }
We'll I guess we'll work around that too... sigh
...
abstract class SomeComponentBaseClass extends Component<IProps, IState> implements ISomeComponent {
...
}
// Not as nice since we're now below the actual definition site and it's a little more verbose
// but hey at least were in the same module and we can check the shape if the use site is in
// external code...
// We now have to decide if we'll use this work around for all classes or only the abstract ones
// and instead mix and match workaround syntax
export const SomeComponentBase: ISomeComponentStatic = SomeComponentBaseClass;
But Wait, There's More! Let's see what happens if we're using generics....
interface IComponent<TProps extends A> { ... }
interface IComponentStatic<TProps extends A> { ... }
interface IComponentClass<TProps extends A, TOptions extends B> extends IComponentStatic<TProps> {
new (options: TOptions): IComponent<TProps> & IComponentStatic<TProps>;
}
abstract class SomeComponentBaseClass<TProps extends A, TOptions extends B> extends Component<TProps, IState> implements IComponent<TProps> {
...
}
// Ruh Roh Raggy: "Generic type .... requires 2 type argument(s) ..."
export const SomeComponentBase: IComponentStatic = SomeComponentBaseClass;
// ".... requires 1 type argument(s) ... " OH NO, We need a different workaround
export const SomeComponent: IComponentStatic =
class extends SomeComponentBase<TProps extends A, ISomeComponentOptions> {
...
}
At this point if you have a use site within code that will be checked by typescript, you should probably just bite the bullet and rely on that even if it is really far away.
If you're use site is external convince the community/maintainers to accept the static
modifier for interface members. And in the meantime you'll need another workaround. It's not at the definition site but I believe you can use a modified version of a workaround described in #8328 to achieve this. See the workaround in the comment from @mhegazy on May 16th 2016 courtesy of @RyanCavanaugh
Forgive me if I'm missing key points. My understanding of the hesitancy to support static
for interface members is primarily a dislike of using the same interface + implements keyword to describe both the shape of the constructor and the shape of the instance.
Let me preface the following analogy by saying I love what typescript offers and greatly appreciate those who've developed it and the community that puts a lot of thought and effort into dealing with the plethora of feature requests; doing their best to implement the features that are a good fit.
But, the hesitancy in this case appears to me as a desire to preserve 'conceptual purity' at the cost of usability. Again sorry if this analogy is way off base:
This reminded of switching from Java
to C#
. When I first saw C#
code doing this
var something = "something";
if(something == "something") {
...
}
the alarm bells started sounding in my head, the world was ending, they needed to be using "something".Equals(something)
, etc
==
is for reference equals! You have a separate .Equals(...)
for string comparison...
and the string literal needs to come first so you don't get a null ref calling .Equals(...) on a null variable...
and... and... hyperventilating
And then after a couple weeks of using C#
I came to realize how much easier it was to use because of that behavior. Even though it meant giving up the clear distinction between the two, it makes such a dramatic improvement in usability that it is worth it.
That is how I feel about the static
modifier for interface members. It will allow us to continue describing the shape of the instance as we already do, allow us to describe the shape of the constructor function which we can only do with poor workarounds case by case, and allow us to do both in relatively clean and easy way. A huge improvement in usability, IMO.
I'll weigh in on abstract static
in the next comment....
Interfaces always require respecification of member types on implenting classes. (inference of member signatures in explicitly implementing classes is a separate, as yet unsupported, feature).
The reason you are getting errors is unrelated to this proposal. To get around it, you need to use the readonly
modifier on the implementation of class members, static or otherwise, that need to be of literal types; elsewise, string
will be inferred.
Again the implements
keyword is just a specifications of intent, it does not influence the structural compatibility of types, nor introduce nominal typing. That's not to say that it couldn't be useful but it doesn't change the types.
So abstract static
... Not Worth It. Too many problems.
You would need new and complex syntax to declare what will already be checked at the use site implicitly (if the use site will be checked by the typescript compiler).
If the use site is external then what you really need are static
members of an interface... Sorry done plugging.... and good night!
@aluanhaddad True, but even if inference of member signatures in explicitly implementing classes were a supported feature we lack clean ways of declaring the member signatures for static side of the class.
The point I was trying to express was that we lack ways to explicitly
declare the expected structure of the static side of the class and there are cases where that does matter (or will matter in order to support features)
Primarily I wanted to refute this part of your comment "it does not matter if the interface(s) that specifies the requirements for the static side of the class is syntactically referenced".
I was trying to use type inference as an example of why it would matter with future support.
By syntactically referencing the interface here let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' }
we don't have to explicitly declare the type as 'Yes' | 'No' 100 separate times.
To achieve the same type inference for the static side of a class (in the future) we will need some way to syntactically reference the interface(s).
After reading your latest comment I think that wasn't as clear an example as I was hoping. Perhaps a better example is when the use site for the static side of the class is in external code that won't be checked by the typescript compiler. In such a case we currently have to rely on workarounds that essentially create an artificial use site, provide poor usability/readability, and do not work in many cases.
At the expense of some verbosity, you can achieve a decent level of type safety:
type HasType<T, Q extends T> = Q;
interface IStatic<X, Y> {
doWork: (input: X) => Y
}
type A_implments_IStatic<X, Y> = HasType<IStatic<X, Y>, typeof A> // OK
type A_implments_IStatic2<X> = HasType<IStatic<X, number>, typeof A> // OK
type A_implments_IStatic3<X> = HasType<IStatic<number, X>, typeof A> // OK
class A<X, Y> {
static doWork<T, U>(_input: T): U {
return null!;
}
}
type B_implments_IStatic = HasType<IStatic<number, string>, typeof B> // Error as expected
class B {
static doWork(n: number) {
return n + n;
}
}
Agreed this should be allowed. Following simple use case would benefit from abstract static methods:
export abstract class Component {
public abstract static READ_FROM(buffer: ByteBuffer): Component;
}
// I want to force my DeckComponent class to implement it's own static ReadFrom method
export class DeckComponent extends Component {
public cardIds: number[];
constructor(cardIds: number[]) {
this.cardIds = cardIds;
}
public static READ_FROM(buffer: ByteBuffer): DeckComponent {
const cardIds: number[] = [...];
return new DeckComponent(cardIds);
}
}
@RyanCavanaugh I don't think anyone has mentioned this exactly yet(although I could have missed it in my quick read-through), but in response to:
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?
This could be addressed by implementing part of #3841. Reading through that issue the primary objection raised seems to be that types of constructor functions of derived classes are often not compatible with the constructor functions of their base classes. However the same concern does not appear to apply to any other static methods or fields because TypeScript is already checking that overridden statics are compatible with their types in the base class.
So what I'd propose is giving T.constructor
the type Function & {{ statics on T }}
. That would allow abstract classes that declare an abstract static foo
field to safely access it via this.constructor.foo
while not causing issues with constructor incompatibilities.
And even if automatically adding the statics to T.constructor
isn't implemented we would still be able to use abstract static
properties in the base class by manually declaring "constructor": typeof AbstractParentClass
.
I think many people expect @patryk-zielinski93 example should work. Instead, we should use counterintuitive, verbose and cryptic workarounds. Since we already have 'syntax sugared' classes and static members, why we cannot have such sugar in type system?
Here is my pain:
abstract class Shape {
className() {
return (<typeof Shape>this.constructor).className;
}
abstract static readonly className: string; // How to achieve it?
}
class Polygon extends Shape {
static readonly className = 'Polygon';
}
class Triangle extends Polygon {
static readonly className = 'Triangle';
}
Maybe we could instead/in parallel introduce a static implements
clause? e.g.
interface Foo {
bar(): number;
}
class Baz static implements Foo {
public static bar() {
return 4;
}
}
This has the advantage of being backwards-compatible with declaring separate interfaces for static and instance members, which seems to be the current workaround of choice, if I'm reading this thread correctly. It's also a subtly different feature which I could see being useful in its own right, but that's beside the point.
(statically implements
would be better, but that introduces a new keyword. Debatable if this is worth that. It could be contextual, though.)
No matter how hard I tried to understand arguments of those who are skeptical about OP's proposal, I failed.
Only one person (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) answered to you @RyanCavanaugh . I rewrote OP's code to make it bit cleaner and also to illustrate your question and my answers:
abstract class Base {
abstract static foo() // ref#1
}
class Child1 extends Base {
static foo() {...} // ref#2
}
class Child2 extends Base {
static foo() {...}
}
Who is allowed to call an abstract static method? Presumably you can't invoke
Base.foo
directly
If you mean ref#1, then yes, it should never be callable, because well It is abstract, and doesn't even have a body. You are only allowed to call ref#2.
But can you invoke the method on an expression of type Base? If so, why should that be allowed?
function bar(baz:Base) { // "expression of type Base" baz.foo() // ref#3 }
function bar2(baz: typeof Base) { // expression of type Base.constructor baz.foo() // ref#4 }
ref#3 is an error. No, you cannot invoke "foo" there, because `baz` supposed to be an __instance__ of Child1 or Child2, and instances do not have static methods
ref#4 is (should be) correct. You can (should be able to) invoke static method foo there, because `baz` supposed to be constructor of Child1 or Child2, which both extend Base and, therefore must have implemented foo().
bar2(Child1) // ok bar2(Child2) // ok
You can imagine this situation:
bar2(Base) // ok, but ref#4 should be red-highlighted with compile error e.g. "Cannot call abstract method." // Don't know if it's possible to implement in compiler, though. // If not, compiler simply should not raise any error and let JS to do it in runtime (Base.foo is not a function).
> What is the use of the feature?
See ref#4 The use is when we do not know which one of child classes we receive as argument (it may be Child1 or Child2), but we want to call it's static method and be sure that that method exists.
I am re-writing node.js framework right know, called AdonisJS. It was written in pure JS, so I am transforming it to TS. I cannot change how code works, I am only adding types. Lacking this features makes me very sad.
p.s. In this comment, for simplicity reasons, I wrote about only abstract classes, and did not mention interfaces. But everything I wrote is applicable to interfaces. You just replace abstract class with interface, and everything will be correct. There is possibility, when TS team for some reason (don't know why) would not want to implement `abstract static` in `abstract class`, however it would be fine to only implement `static` word in interfaces, that would make us happy enough, I guess.
p.p.s. I edited class and method names in your questions to comply them with my code.
Couple of thoughts, based on other skeptics' main arguments:
"No, this must not be implemented, but you already can do it this way *writes 15 times more lines of code*".
That's what syntactical sugar is made for. Let's add a little (sugar) to TS. But no, we'd better torture developers' brains who try to break through decorators and dozen of generics instead of adding one simple static
word to interfaces. Why not consider that static
as another sugar to make our lives easier? Same way as ES6 adds class
(which becomes ubiquitous nowadays). But no, let us be nerds and do things old and "right" way.
"There are two interfaces for js classes: for constructor and instance".
Ok, why not give us a way to make interface for constructor then? However simply adding that static
word is much easier, and more intuitive.
And regarding this:
Holding off on this until we hear more feedback on it.
More than one year have passed, and plenty of feedback provided, for how long this issue is going to be held off? Can someone answer, please.
This post may seem kinda harsh... But no, it's a request from the guy who loves TS and at the same time truely cannot find one solid reason why we should go with ugly hacks, when converting our old JS codebase to TS. Apart from this, huuuge thanks to TS team. TS is just wonderful, it changed my life and I enjoy coding and my job more than ever... But this issue is poisoning my experience..
Possible workaround?
export type Constructor<T> = new (...args: any[]) => T;
declare global {
interface Function extends StaticInterface {
}
}
export interface StaticInterface {
builder: (this: Constructor<MyClass>) => MyClass
}
// TODO add decorator that adds "builder" implementation
export class MyClass {
name = "Ayyy"
}
export class OtherClass {
id = "Yaaa"
}
MyClass.builder() // ok
OtherClass.builder() // error
interface Applicative<A> extends Apply<A> {
static of<A>(a: A): Applicative<A>;
}
const of = <A, C extends static Applicative<A>>(c: C, a: A): new C => c.of(a);
Serializable
type with static deserialize
methodinterface Serializable {
static deserialize(s: string): Serializable;
serialize(): string;
}
interface Contract {
static create(x: number): Contract;
static new(x: number): Contract;
}
const factory1 = (c: static Contract): Contract => c.create(Math.random());
const factory2 = (C: static Contract): Contract => new C(Math.random());
const factory3 = <C extends static Contract>(c: C): new C => c.create(Math.random());
const c1 = factory1(ContractImpl); // `Contract`
const c2 = factory2(ContractImpl); // `Contract`
const c3 = factory3(ContractImpl); // `ContractImpl`
interface Serializable {
static deserialize(s: string): Serializable;
}
type Serializable = {
static deserialize(s: string): Serializable;
};
abstract class Serializable {
static abstract deserialize(s: string): Serializable;
}
interface Contract {
static new(): Contract;
}
Discuss:
How static call signatures may be expressed?
If we will express them with simply adding static
modifier before call signature,
how we will distinct them from instance method with name 'static'
?
Can we use the same workaround as for 'new'
-named methods?
In such case, it will be definitely a breaking change.
static
type operatorconst deserialize = (Class: static Serializable, s: string): Serializable =>
Class.deserialize(s);
new
type operatorconst deserialize = <C extends static Serializable>(Class: C, s: string): new C =>
Class.deserialize(s);
[[Static]]
internal slot“Static” interface of a type will be stored in the [[Static]]
internal slot:
// The type
interface Serializable {
static deserialize(s: string): Serializable;
serialize: string;
}
// will be internally represented as
// interface Serializable {
// [[Static]]: {
// [[Instance]]: Serializable; // See below.
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
By default have type never
.
[[Instance]]
internal slot“Instance” interface of the type will be stored
in the [[Instance]]
internal slot of the [[Static]]
internal slot type.
By default have type never
.
static
operatorWhen a type used as a value type,
the [[Static]]
internal slot will be discarded:
declare const serializable: Serializable;
type T = typeof serializable;
// { serialize(): string; }
But the type itself remains keep the [[Static]]
internal slot:
type T = Serializable;
// {
// [[Static]]: {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
Both values of static-aware types are assignable to the structurally identical
(except of the [[Static]]
, of course) ones and vice versa.
static
operatorAssociativity | Precedence |
---|---|
Right | IDK, but equals to the new 's one |
The static
type operator returns the type of [[Static]]
internal slot of the type.
It somewhat similar to the typeof
type operator,
but it's argument must be a type rather than a value.
type T = static Serializable;
// {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// }
The typeof
type operator also discards the [[Instance]]
internal slot:
declare const SerializableImpl: static Serializable;
type T = typeof SerializableImpl;
// { deserialize(s: string): Serializable; }
Both values of instance-aware types are assignable to the structurally identical
(except the [[Instance]]
internal slot, of course) ones and vice versa.
new
operatorAssociativity | Precedence |
---|---|
Right | IDK, but equals to the static 's one |
The new
operators returns the type of [[Instance]]
internal slot of the type.
It effectively reverses the static
operator:
type T = new static Serializable;
// {
// [[Static]]: {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
extends
/implements
semanticsA class implementing an interface with non-default [[Static]]
internal slot
MUST have a compatible [[Static]]
internal slot.
The compatibility checks should be the same as (or similar with)
regular (instance) compatibility checks.
class SerializableImpl implements Serializable {
static deserialize(s: string): SerializableImpl {
// Deserializing logic goes here.
}
// ...other static members
// constructor
// ...other members
serialize(): string {
//
}
}
infer
operator?I've spent a few hours reading through this issue and other issues for a workaround for my problem, which would be solved by the static modifier or interfaces for classes. I can't find a single workaround which solves my problem.
The issue I have is that I want to make a modification to a code generated class. For example what I would like to do is:
import {Message} from "../protobuf";
declare module "../protobuf" {
interface Message {
static factory: (params: MessageParams) => Message
}
}
Message.factory = function(params: MessageParams) {
const message = new Message();
//... set up properties
return message;
}
export default Message;
I can't find a single workaround that allows me to do the equivalent of this, with the current version of TS. Am I missing a way to solve this problem currently?
This feels relevant to post here as a use case for which there is apparently no workaround and certainly no straightforward workaround.
If you want to check the instance and static side of a class against interfaces, you can do it like this:
interface C1Instance {
// Instance properties ...
// Prototype properties ...
}
interface C2Instance extends C1Instance {
// Instance properties ...
// Prototype properties ...
}
interface C1Constructor {
new (): C1Instance;
// Static properties ...
}
interface C2Constructor {
new (): C2Instance;
// Static properties ...
}
type C1 = C1Instance;
let C1: C1Constructor = class {};
type C2 = C2Instance;
let C2: C2Constructor = class extends C1 {};
let c1: C1 = new C1();
let c2: C2 = new C2();
All too many of us are wasting far too many hours of our time and others on this. Why isn't it a thing?!¿i! Why are the answers all huge workarounds for something that should be readable, digestible, and something that's a 1-liner that's straightforward. There's no way I want someone to have to figure out what my code is trying to do with some hacky workaround. Sorry to further clutter this topic with something that's not very valuable, but it's a huge pain and waste of time currently, so I do find it valuable for me, the others right now, and those who come along later looking for an answer.
The thing with any language, both natural and artificial, that it should naturally evolve to be efficient and appealing to be used. Eventually people decided to use reductions in language, ("okay"=>"ok","going to" =>"gonna"), invented new ridiculous words, like "selfie" and "google", redefined the spelling with l33tspeak and stuff, and even banned some words, and, despite if you want to use them or not, everybody still kinda understands what they mean, and some of us do use them to achieve some particular whatever tasks. And for none of those there could be any good reason, but the efficiency of certain people in certain tasks, it's all question of numbers of people, who actually make use of them. The volume of this conversation clearly shows, that a whole lot of people could make a use of this static abstract
for whatever goddamn considerations they have. I came here for the same reason, 'cause I wanted to implement Serializable
so I tried all of the intuitive (for me) ways to do it, and none of them work. Trust me, the last thing I would look for is the explanation why I do not need this feature and should go for something else. Year and a half, jesus christ! I bet there's a PR somewhere already, with tests and stuff. Please make this happen, and if there's a certain way of usage which is discouraged, we have a tslint for that.
It is possible to access static members of child classes from the base class via this.constructor.staticMember
, so abstract static members make sense to me.
class A {
f() {
console.log(this.constructor.x)
}
}
class B extends A {
static x = "b"
}
const b = new B
b.f() // logs "b"
Class A should be able to specify that it requires static x
member, because it uses it in f
method.
Any news ?
The features are really needed 😄
1) static
functions in interfaces
2) abstract static
functions in abstract classes
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.