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

Add support Interfaces to define static methods #13462

Closed
Serginho opened this issue Jan 13, 2017 · 43 comments
Closed

Add support Interfaces to define static methods #13462

Serginho opened this issue Jan 13, 2017 · 43 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@Serginho
Copy link

TypeScript Version: 2.0.3

Code

interface Foo {
public static myStaticMethod(param1: any);
}

Expected behavior:
No errors
Actual behavior:
Unsupported

@Serginho Serginho changed the title Add support to define static methods in Interfaces Add support Interfaces to define static methods Jan 13, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Jan 13, 2017

What do you wan to achieve with this? can you elaborate on the scenario?

@rozzzly
Copy link

rozzzly commented Jan 14, 2017

I imagine abstract classes are what you're looking for.

@Serginho
Copy link
Author

Serginho commented Jan 14, 2017

@mhegazy
Example:

interface JsonSerializable {
      public static fromJson(obj: any);
      public toJson(): string;
}

@rozzzly In this example, abstract class is not valid.

I think this would be pretty cool! Java added this functionality in the last version.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 15, 2017

@Serginho
I think you may find this interesting:

interface JsonSerializableStatic<C extends new (...args) => JsonSerializable<C>> {
  fromJson(json: string): JsonSerializable<C>;
}

interface JsonSerializable<C extends new (...args) => any> {
  toJson: () => string;
  constructor: C;
}

interface A extends JsonSerializable<typeof A> { }
class A implements JsonSerializable<typeof A> {

  constructor(readonly id: number, readonly name: string) { }
  toJson() { return JSON.stringify(this); }

  static fromJson(json: string): A {
    const obj = JSON.parse(json);
    return new A(obj.id, obj.name);
  }
}

const a = new A(1, 'Charlize');

const json = a.toJson();

const y = A.fromJson(json);
console.info(a, json, y);
console.info(new a.constructor(1, 'Theron'));
const m = new A.prototype.constructor(1, 'Charlize Theron');
console.info(m);

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Jan 16, 2017
@Serginho
Copy link
Author

Serginho commented Jan 16, 2017

@aluanhaddad I find your code a workaround of something which is obvious.

interface JsonSerializable {
      public static fromJson(obj: any);
      public toJson(): string;
}

Which do you see more clearly?

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 16, 2017

@aluanhaddad I find your code a workaround of something which is obvious.

interface JsonSerializable {
public static fromJson(obj: any);
public toJson(): string;
}
Which do you see more clearly?

Indeed, but mine is not a workaround, it is a justification for your usecase. Having static deserialization and instance serialization implemented on a class by class basis and thereby encapsulated and typesafe.
Your declaration:

interface JsonSerializable {
     public static fromJson(obj: any);
     public toJson(): string;
}

is irrelevant as it basically re-declares the interface of the JSON object. That absolutely does not require static interface methods in any way.

@Serginho
Copy link
Author

You are loosing the concept of Interface. class A implements JsonSerializable should make me implementing both methods. But actually makes me to implement:

toJson: () => string;
constructor: new (...args) => JsonSerializableStatic<C>;

It's not a clear solution.

There is no technical reason for not allowing to define static methods on interfaces.

@aluanhaddad
Copy link
Contributor

@Serginho I am not arguing that the situation is ideal. I was just trying to illustrate that it can be expressed.

@Serginho
Copy link
Author

@aluanhaddad Come on! Open your mind. Do you think typescript should allow static methods in Interfaces? this was implemented in Java 8 in the last version, so I think I'm not talking nonsense.

@gcnew
Copy link
Contributor

gcnew commented Jan 16, 2017

@Serginho I don't think it's a particularly good fit for TypeScript. 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. Interweaving the two doesn't feel right from a design point to me.

As @aluanhaddad already wrote, TypeScript actually already has a mechanism to express what you desire. The big difference is that in this case you treat the class as an object, which continues to be logically consistent. From my perspective, the approach you propose is not particularly well suited to TypeScript (and JavaScript development) as classes are kind of a hack/second class citizens. The currently prevailing patterns count on ducktyping/shape programming, not on rigid classes.

I guess, you may open your mind as well and try different styles of programming. The world neither starts nor ends with OOP. You may find programming with functions, plain objects and (and to some degree) prototypes pleasurable and even better. Such a style is much more in line with what JavaScript was initially designed for. On a side note, I agree JS is slowly shifting towards OOP-esque patterns (at least on the committee level), but that's because of the huge push of people not being comfortable with different programming paradigms and development techniques. For me it's a deeply saddening and disappointing thing.

@andy-hanson
Copy link

andy-hanson commented Jan 28, 2017

As @gcnew said, an interface describes the shape of an individual object, not its class. But there's no reason we can't use an interface to describe the shape of the class itself, since classes are objects too.

import assert = require("assert");

interface JsonSerializableStatic<JsonType, InstanceType extends JsonSerializable<JsonType>> {
    fromJson(obj: JsonType): InstanceType;
}
interface JsonSerializable<JsonType> {
    toJson(): JsonType;
}

interface PointJson { x: number; y: number; }
class Point /*static implements JsonSerializableStatic<PointJson, Point>*/ {
    static fromJson(obj: PointJson): Point {
        return new Point(obj.x, obj.y)
    }

    constructor(readonly x: number, readonly y: number) {}

    toJson(): PointJson {
        return { x: this.x, y: this.y };
    }
}
// Hack for 'static implements'
const _: JsonSerializableStatic<PointJson, Point> = Point;

function testSerialization<JsonType, InstanceType extends JsonSerializable<JsonType>>(cls: JsonSerializableStatic<JsonType, InstanceType>, json: JsonType) {
    const instance: InstanceType = cls.fromJson(json);
    const outJson: JsonType = instance.toJson();
    assert.deepEqual(json, outJson);
}
testSerialization(Point, { x: 1, y: 2 });

Failing to meet the signature of either toJson or fromJson results in a compile error. (Unfortunately, the error is at const _ rather than at the method.)

Probably related (since it deals with typing static methods): #5863.


@aluanhaddad: Your example has a mistake: JsonSerializable's constructor member would actually refer to an instance property called constructor, which when invoked with new would return a JsonSerializableStatic. What that would mean is that (new ((new X()).constructor)).fromJson({}) would have to work. The reason it compiles successfully is that interface A extends JsonSerializable<typeof A> declares the implementation to be valid without actually checking it. For example, this compiles without error:

interface I { m(): void; }
interface A extends I { }
// No compile error
class A implements I { em() {} }

@Serginho Not a Java user, but it doesn't look like the language allows you to define an interface for the static side of a class (meaning classes implementing the interface would have to implement a static method to conform). Java allows you to define a static method with a body in an interface, the TypeScript equivalent of which would be:

interface I { ... }
namespace I {
   export function interfaceStaticMethod() {}
}

@RLovelett
Copy link

What if you were trying to make a generic factory?

interface Factorizable {
  static factory<U>(str: string): U
}

class Foo {
  private data: string[] = []
  bar<T extends Factorizable>(): T[] {
    return this.data.map(T.factory);
  }
}

class Bar implements Factorizable {
  static factory(str: string): Bar {
    // ...
  }
}

// Usage
var x = new Foo();
var y: Bar[] = x.bar();

I'm not sure about the interface syntax I'm proposing here but I know the "usage" syntax is what I'm trying to achieve. This is a pattern I use frequently in Swift (using protocols) and I think it would be really nice in TypeScript. Though not being a language designer or compiler implementor I'm not sure if it fits the intended direction of TypeScript or is realistic to be implemented.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 4, 2017

The class does not have to implement the interface. you just define the interface, and structural type checks will cache any issues at usage site, e.g.:

interface Factorizable<U> {
    factory(str: string): U
}

class Foo {
  private data: string[] = []
  bar<T>(factory: Factorizable<T>): T[] {
    return this.data.map(factory.factory);
  }
}

class Bar {
  static factory(str: string): Bar {
    // ...
  }
}

// Usage
var x = new Foo();
var y = x.bar(Bar); // Bar[]

@RLovelett
Copy link

RLovelett commented Feb 7, 2017

@mhegazy that is a relatively good solution. Thank you for providing that! 🙏

There are still two things that give me discomfort (admittedly neither are show stoppers):

  1. We still aren't able to declare that Bar explicitly implements or conforms to Factorizable.

    • In practice, I imagine this really isn't a problem. Since It would be true that if the Factorizable interface changes in an incompatible way the usage x.bar(Bar) would begin to error and then you'd correct the drift changes.
  2. For me, it is still a cognitive burden to declare the type of y on the right side of the assignment.

    • Weirder still this syntax would enable this behavior: var y: Baz[] = x.bar(Bar). Obviously, an error, but the syntax is enabling the developer to over-constrain the problem by defining the return type in two places.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 7, 2017

We still aren't able to declare that Bar explicitly implements or conforms to Factorizable.

There are two types involved, 1. constructor function (e.g. static side of the the class), and 2. the instance side (what comes out when calling new). Mixing these two in one type is not correct. In theory you can have implements and static implements but in practice, as you noted, this is seldom used, and the implements clause really does not add much. The check is done at use site any ways regardless if you have an implements clause or not.

For me, it is still a cognitive burden to declare the type of y on the right side of the assignment.

The two mean different things, var y = x.bar(Bar) declares a new variable y with the same type as x.bar(Bar); where as var y: Bar[] = x.bar(Bar) declares a new variable y with type Bar[] and verifies that the type of x.bar(Bar) is assignable to Bar[].

Having siad that, this is rather a style issue. Personally, my recommendation is to not use type annotations explicitly unless you have to; let the types flow through he system. I have seen code bases, however, where the style guide is the opposite, where everything has an explicit type annotation.

@RLovelett
Copy link

@mhegazy thank you for the discussion/perspective.

@aluanhaddad
Copy link
Contributor

@andy-hanson Thanks for taking the time to correct me. I fixed the error in my example.

@Val0429
Copy link

Val0429 commented Apr 20, 2017

This also works, and show error at compile time without any extra function call:

interface Type<T> {
    new (...args: any[]): T;
}

/* static interface declaration */
interface ComparableStatic<T> extends Type<Comparable<T>> {
    compare(a: T, b: T): number;
}

/* interface declaration */
interface Comparable<T> {
    compare(a: T): number;
}

/* class decorator */
function staticImplements<T>() {
    return (constructor: T) => {}
}

@staticImplements<ComparableStatic<TableCell>>()   /* this statement implements both normal interface & static interface */
class TableCell { /* implements Comparable<TableCell> { */  /* not required. become optional */
    value: number;

    compare(a: TableCell): number {
        return this.value - a.value;
    }

    static compare(a: TableCell, b: TableCell): number {
        return a.value - b.value;
    }
}

@mhegazy mhegazy closed this as completed Apr 24, 2017
@huan
Copy link

huan commented May 14, 2017

What's the result of the discussion?

I ran into this and I also want to use static interface:

interface IDb {
  public static instance: () => Db,
}

@kitsonk
Copy link
Contributor

kitsonk commented May 14, 2017

Most people forget that there are already static interfaces in the sense that a constructor function/class has two interfaces already, the constructor interface and the instance interface.

interface MyFoo {
  method(): void;
}

interface MyFooConstructor {
  new (): MyFoo;
  prototype: MyFoo;
  staticMethod(): any;
}

const MyFoo = function MyFoo() {
  this.prop = '';
} as any as MyFooConstructor;

MyFoo.prototype = {
  method() { console.log(this); }
}

MyFoo.staticMethod = function () { /* do something static */ }

If you aren't using abstract classes, then you already have the power.

@huan
Copy link

huan commented May 15, 2017

Thank @kitsonk for replying.

Your declaration seems should work, but it's too verbose for the case.

And I just tried abstract classes, but it seems not support static with abstract.

[ts] 'static' modifier cannot be used with 'abstract' modifier.

@kitsonk
Copy link
Contributor

kitsonk commented May 15, 2017

@zixia that is issue #14600

@huan
Copy link

huan commented May 15, 2017

Yeah, let's vote it up.

@grantila
Copy link

Would anybody care to reply with a solution to my question here: http://stackoverflow.com/questions/44047874/dynamically-modify-typescript-classes-through-a-generic-function

I think that whole issue would be solved by being able to specify static members on interfaces, and if this issue is closed because it's not needed, I would very much like to see how to solve it instead.

@Enet4
Copy link

Enet4 commented May 18, 2017

@grantila I have answered your question. As previously mentioned in this issue, unless you have further requirements not mentioned there, this is easily solvable by treating classes as objects.

@grantila
Copy link

@Enet4 I updated the question, it was overly-simplified. The real issue cannot be fixed by the Object.defineProperty() hack unfortunately. Which btw, is a hack. I want to ensure not to having misspelled make - basically proper static checking.

This is a real problem I have, code that I began porting to TypeScript but now have kept as JavaScript, since I currently cannot rewrite such massive amounts of code as would have been necessary otherwise.

@Enet4
Copy link

Enet4 commented May 18, 2017

I want to ensure not to having misspelled make - basically proper static checking.

Static checking can only go so far here. But if your only concern now is that you are setting the right property, then casting to a maker type and setting the property from there seems to address that.

@grantila
Copy link

@Enet4 that's a working solution, thanks. I think this issue (13462) should be looked at again, as solutions like yours, using type casting, is actually not type safe, and if this is the only way to solve the situation of working with the class type as a value, we're losing a lot of the flexibility of a dynamic language.

@Enet4
Copy link

Enet4 commented May 19, 2017

solutions like yours, using type casting, is actually not type safe, and if this is the only way to solve the situation of working with the class type as a value, we're losing a lot of the flexibility of a dynamic language.

@grantila In my defense, that is debatable. 😉 Your use case is distinct from those presented in this issue, in that your class type may (or not) provide a method depending on runtime conditions. And IMO that is more unsafe than the type cast presented in my answer, which was only performed to allow the insertion of a field in an object. In that sense, the resulting class type C & Maker<T> should remain compatible with everything else relying on a C.

I also tried picturing where static methods in interfaces could help you here, but I might be missing something. Even if you had something like a static make?(... args: any[]): self in your interface, it would have to be checked in runtime for existence before a call. If you would like to continue this discussion, let's consider doing so elsewhere to reduce noise. 🙂

@tyteen4a03
Copy link

So we cannot type check static factory methods in classes that implement the same interface?

My use case is:

interface IObject {
    static make(s: string): IObject;
}

class A implements IObject{
    static make(s: string): IObject {
        // Implementation A...
    }
}

class B implements IObject{
    static make(s: string): IObject {
        // Implementation B...
    }
}

A.make("string"); // returns A
B.make("string"); // returns B

I don't want to write a new factory class just for this.

@ghost
Copy link

ghost commented Aug 15, 2017

@tyteen4a03 Remove IObject from that example, and it will compile. See also #17545 (comment)

@tommybananas
Copy link

@andy-ms Yes obviously it works but the entire point of type checking is to.. check types. You can always degrade type safety far enough to make every use case compile, but that is ignoring the fact that this is a feature request and a not-so-crazy one at that.

@kitsonk
Copy link
Contributor

kitsonk commented Dec 1, 2017

Yes obviously it works but the entire point of type checking is to.. check types.

This is a long long long thread on how the static side of class is a separate interface to the instance side and implements points to the instance side. @andy-ms was indicating to @tyteen4a03 how to get a snippet of code working, because it was wrong, not to forgo type checking.

@rmblstrp
Copy link

rmblstrp commented Dec 7, 2017

My use case for allowing static methods to use the class generic parameter is for mixin classes. I am building an entity framework which uses annotations to define columns, databases, etc and would like to have a static function mixed in to my entity classes which would allow convenient access to the correctly typed repository.

class RepositoryMixin<T> {
    public static repository(): EntityRepository<T> {
        return new EntityRepository<T>(Object.getPrototypeOf(this));
    }
}

@mixin(RepositoryMixin)
class Entity implements RepositoryMixin<Entity> {
    public id: number;
}

Entity.repository().save(new Entity());

@Enet4
Copy link

Enet4 commented Dec 7, 2017

@rmblstrp Can you show how you would use the proposed feature in your example? Preferably as something verifiable?

@aluanhaddad
Copy link
Contributor

@rmblstrp that doesn't require this feature. In fact, since you are using a decorator, you can actually is its type to verify that the annotated classes provides required static methods. You don't need both and it would actually be rather redundant.

@Deviad
Copy link

Deviad commented Mar 10, 2018

Hello, I don't wanna be out of topic or out of the scope of this conversation.
However, since you brought in discussion different programming paradigms (should we use OOP or functional?), I want to talk specifically about static factories which are used usually to create a connection to a db or provide some kind of service.
In many languages like PHP and Java, static factories have been like deprecated in favor of Dependency Injection. Dependency Injection and IOC containers have been made popular thanks to frameworks like Symfony and Spring.
Typescript has a wonderful IOC container named InversifyJS.

These are the files where you can see how the whole thing is handled.
https://github.com/Deviad/virtual-life/blob/master/models/generic.ts
https://github.com/Deviad/virtual-life/blob/master/service/user.ts
https://github.com/Deviad/virtual-life/blob/master/models/user.ts
https://github.com/Deviad/virtual-life/blob/master/utils/sqldb/client.ts
https://github.com/Deviad/virtual-life/blob/master/bootstrap.ts

I am not saying it's a perfect solution (and probably it leaves some scenarios uncovered) but it works also with React, there some examples already you can look at.

Also, I suggest you to look at this video about functional programming that covers this aspect about which one is better: https://www.youtube.com/watch?v=e-5obm1G_FY&t=1487s
SPOILER: no one is better, it depends on the problem you want to solve. If you deal with users, classrooms, teachers OOP will be better to model your problem using objects.
If you need a parser that scans a website maybe functional can be better to use function generators that return partial results, etc. :)

@rmblstrp
Copy link

@Deviad @aluanhaddad Since my post I have been using InversifyJS and it has been absolutely great and definitely a much better way to go. At the time of my post I had just started using Typescript/Node after having been PHP/C# previously. It just took a little bit of time getting familiar with the environment and available packages.

@HunderlineK
Copy link

What is the status of this? Why do you keep closing unresolved issues in the repo?

@Enet4
Copy link

Enet4 commented Jun 23, 2018

I believe this is one of those cases where the issue should be explicitly labelled with "wontfix", as the choice of not having static methods in interfaces is by design.

@fbartho
Copy link

fbartho commented Jun 23, 2018

@Enet4, I’m a newcomer, but that wasn’t clear at all. Reading this and other related issues, it sounds like it mostly the following issues:

A. It’s hard
B. We don’t like (each) specific syntax we’ve seen so far.
C. A small minority don’t believe it should be doable at all, and would rather delete the current weird way to do it.

@fbartho
Copy link

fbartho commented Jun 23, 2018

If it’s actually by design, and those in charge don’t want it, they should write a public doc, and cross link it into this and other threads. This would save us a lot of time over keeping us in limbo.

@RyanCavanaugh
Copy link
Member

We have already linked to #14600 in this thread and that is the issue to follow for the feature request.

@microsoft microsoft locked and limited conversation to collaborators Jun 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests