microsoft / TypeScript

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

Allow specifying interface implements clauses for the static side of classes #33892

Open hdodov opened 5 years ago

hdodov commented 5 years ago

Search Terms

class static side syntax interface type expression

Suggestion

Currently, you can only specify the static side interface of a class with a declaration. From the handbook:

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
      console.log("beep beep");
  }
}

When I first wanted to do this (before looking at the docs), I tried to do it in this fashion:

class Clock: ClockConstructor implements ClockInterface {
  ...
}

And I was surprised to see that it didn't work. My proposal is to make this a valid syntax as it's more intuitive and understandable.

I believe that forcing class expressions conflicts with TypeScript's design goals:

  1. Produce a language that is composable and easy to reason about.

Why use a class expression when there is no need for it? Why change your actual JavaScript logic for something that exists only in TypeScript and not in your production code.

Use Cases

Anywhere you need to set the interface of the static side of a class without having a need to specify it as an expression.

Examples

Take the example from the playground:

interface ClockConstructor {
  new (hour: number, minute: number);
}

interface ClockInterface {
  tick();
}

class Clock: ClockConstructor implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
      console.log("beep beep");
  }
}

Checklist

My suggestion meets these guidelines:

RyanCavanaugh commented 5 years ago

Ref #14600

We discussed #14600 at length and agreed that the syntax static T in the implements list of a class would be a reasonable place to allow this:

interface X {
  x: string;
}
interface Y {
  y: string;
}
interface Z {
  z: string;
}

// OK example
class C implements Y, static X, Z {
  static x: string = "ok";
  y = "";
  z = "";
}

// Error, property 'x' doesn't exist on 'typeof D'
class D implements static X {
}

Why not static members in interfaces?

14600 proposed the syntax

interface X {
  static x: string;
}

This is problematic for a couple reasons.

First, it seems to create a totally meaningless thing:

interface X {
  static x: string;
}
function fn(arg: X) {
  // arg has... no members?
}
// Is this a legal call?
// There doesn't seem to be any reason to reject it,
// since 'fn' can't illegally access anything that doesn't exist
fn({ });

Second, there would be no way to access a static member through an interface type that declared it!

interface X {
  static x: string;
}
function fn(arg: X) {
  // How do I get to ctor(arg).x ?
}

You would need to have some auxiliary keyword to turn an interface into the static version of itself:

interface X {
  static x: string;
}
// Syntax: ????
function fn(arg: unstatic X) {
  arg.x; // OK
}

Simply moving the static logic to the class syntax makes this substantially simpler, and (critically) allows classes to implement types that weren't written using the hypothetical interface I { static property: T } syntax.

Why implements static T, not static implements T ?

In the presumably-usual case of one instance and one static heritage member, this is substantially cleaner:

class WidgetMaker implements Factory, static FactoryCtor {
  // ...
}

vs

class WidgetMaker implements Factory static implements FactoryCtor {
  // ...
}
hdodov commented 5 years ago

@RyanCavanaugh I understand your points and agree with all of them. But I don't understand what's the issue with my proposed syntax without the static keyword. I mean, instead of this:

class C implements Y, static X, Z { }

Why not this:

class C: X, Z implements Y { }

Your syntax could be read the correct way like this:

class C implements (Y), static (X, Z)

but due to the commas, it could also be read like this:

class C (implements Y), (static X), (Z)

which would make Z appear out of place. It could even be read like this:

class C implements (Y, static X, Z)

as if C implements Y, Z and a static X which makes no sense. I think the issue here is that the comma is used as both a separator for symbols and keywords.

My proposal has a clear separation of the static and instance sides:

class |C: X, Z| implements |Y, T| { ... }
------|static--------------|instance-----

Yours doesn't:

class |C| implements |Y, T|, static |X, Z| { ... }
------|static--------|instance------|static-------

I just think my proposal leaves less ambiguity.

Some other benefits:

And to clarify, I'm talking about syntax only. No functional differences.

RyanCavanaugh commented 5 years ago

: in TypeScript always means "is of type", but implements has a different meaning ("is assignable to"). IOW the implied semantics of : are

interface P {
  x: number;
}
class C: P {
  static x: number;
  static y: number;
}
C.x; // OK
C.y; // Not OK; only members of P are visible

the same way that this works (disregarding excess property checks):

interface P {
  x: number;
}
const C: P = { x: 0, y: 0 };
C.x; // OK
C.y; // Not OK; only members of P are visible
hdodov commented 5 years ago

I see. To clarify, would static act as a modifier? If I have:

class C implements Y, static X, Z { }

I should read it as "C implements interface Y, interface X as static, and interface Z" instead of "C implements interface Y and interfaces X and Z as static," right?

When I initially read your comment, I thought static would act like implements and you list items after it. Instead, it would act as a modifier to the items of implements?

So if I have an interface X that must be type-checked in both the instance side and the static side, I would do:

class C implements X, static X { }

Is that right?

thw0rted commented 5 years ago

I don't really follow Ryan's goals in his second post here.

it seems to create a totally meaningless thing: // arg has... no members?

If you made an class with only static methods then you couldn't do much with instances of it either.

there would be no way to access a static member through an interface type that declared it

I don't think that's the point. The reason I want to see static in an interface is to avoid needing to split a mix-in definition:

interface Widget {
  static create(): Widget;
  x: number;
}

class A implements Widget {
  static create(): A { ... }
  x: number = 1;
}

function factory(clazz: {create(): Widget}): Widget {
  const ret = clazz.create();
  ret.x = 10;
  return ret;
}

This is cleaner than the alternative, where I'd have to have separate a WidgetConstructor interface and static implement it. That feels clunky and leads to the potential confusing cases @hdodov lists a few posts back, where there isn't really a "right" way to visually parse the sequence of keywords.

hdodov commented 5 years ago

@thw0rted the constructor and the instance are two completely separate entities. In JavaScript, the API of one is not connected in any way to the API of the other. Because they have different APIs, they probably function differently as wellโ€”you can't use a constructor as an instance and vice-versa. Therefore it makes sense to define their types with two separate interfaces.

Your example could be rewritten as:

interface Widget {
  x: number
}

interface WidgetCtor {
  create(): Widget
}

class A implements Widget, static WidgetCtor {
  static create(): A { ... }
  x: number = 1;
}

I personally think that's better.

Even in your example, the Widget interface has a method create() that should return a Widget interface... that should also have create() and return a Widget with create() and so forth? Separating those types in two interfaces solves that problem.


Also, what if you have an object that is Widget-like? If you do let obj: Widget it becomes really confusing that this object uses an interface that has a static create() method. By adding a static member to an interface, you imply that it has a static and an instance side and can therefore be used in classes only. Why limit yourself like that? By specifying two interfaces, you avoid that problem too.

thw0rted commented 5 years ago

You say they're not connected in any way, but you still write both constructor (static) methods and instance methods inside the same class { } block, right? That's what I'm saying in my previous comment. There's a certain parallelism in the single-interface version, one interface that describes (a portion of) one class, that's lacking in the two-interface version you describe.

As for the static create() method of let obj: Widget, can't I call obj.constructor.create() if I want to? I'm not an expert in how ES6 classes differ from constructor functions with prototypal inheritance, but I can say that this at least works in Chrome and Node, whether or not it's "correct".

hdodov commented 5 years ago

but you still write both constructor (static) methods and instance methods inside the same class { } block

Yes, because in terms of JavaScript, you have no reason to separate them. When a class extends another class, it extends both the instance and the static side. Don't forget that classes are just syntactic sugar. In the end, all static members appear on the class (constructor) itself, while the instance methods appear in its prototype. So they are separated in JavaScript as well.

There's a certain parallelism in the single-interface version, one interface that describes (a portion of) one class

Yes, but what value does that give you? You can't describe one class. You describe one constructor and one prototype. You describe two things at once which, in the realm of types, complicates things. Logically, they describe "one class" but they serve a different purpose. One might even say, they're different types. And types is what TypeScript is interested in.

can't I call obj.constructor.create()

Yes, you can. But that's different from obj.create(). A class X with static create() would have its method called with X.create(). Saying an object has the same interface would imply that it should have obj.create(), right? But (back to your example) if static properties are assigned to obj, where should the instance member x be expected? obj.x? Why are suddenly static and instance referring to the same thing? Because we united them in an interface and we shouldn't.

RyanCavanaugh commented 5 years ago

@thw0rted Notice these two lines in your code:

interface Widget {
  static create(): Widget;
//     ~~~~~~~~~~~~~~~~~~
  x: number;
}

class A implements Widget {
  static create(): A { ... }
  x: number = 1;
}

function factory(clazz: {create(): Widget}): Widget {
//                      ~~~~~~~~~~~~~~~~~~
  const ret = clazz.create();
  ret.x = 10;
  return ret;
}

This repetition is exactly what we want to avoid - you defined static create in interface Widget, and have no way to reference that declaration to define the shape of clazz.

thw0rted commented 5 years ago

Don't forget that classes are just syntactic sugar. In the end, all static members appear on the class (constructor) itself, while the instance methods appear in its prototype.

ES6 already has first-class support in modern runtimes.

// In Chrome dev tools console
class XYZ { static a(){return 1;} }
let x = new XYZ();
"" + x.constructor; // "class XYZ { static a(){return 1;} }"

It might just be syntactic sugar once you're down in the guts of the engine, but all the seams have been fully plastered over when viewed from the outside. The whole point of ES6 classes is to stop the dichotomy of "one constructor and one prototype".

I don't follow your last paragraph about "where instance member x is expected". An instance of a class that implements an interface with static properties would not have those properties, because that's not what static means. Maybe a more concrete example would clarify your concerns?

thw0rted commented 5 years ago

Ryan, I take your point about duplication. You're saying that having a separate name for the static/constructor interface allows us to reference it easily in the function parameter type. The cost of your solution is adding a new keyword for "implements static" or similar, which introduces the ambiguities discussed in comment 3 above.

What if instead we could make a conditional type that took an interface with static properties, and returned an interface with only the static properties except now they're not static? I've never had the knack for the type math needed to make complex transformations in the type space -- I won't bother Dragomir with an at-mention again, but he knows what's up -- but maybe it's possible today. If not, maybe adding type operators to make it possible would be a useful contribution to the language. Imagine:

interface Widget { static create(): Widget; x: number; }
type StaticShape<T> = โœจ ; // magic!

type WidgetConstructor = StaticShape<Widget>; // === interface { create(): Widget }

function factory(clazz: WidgetConstructor): Widget { ... }

Bonus points if the magic can turn a class with constructor(x, y, z) into a new(x, y, z) property, which has been a sticking point for factory-pattern for a while, too. Of course, since I'm hand-waving the hard part here, I recognize that this could be completely impractical, but I figured it's worth asking. And if it works, it has the virtue of avoiding new, potentially-ambiguous keywords while closely mirroring the existing (static class method) syntax.

hdodov commented 5 years ago

@thw0rted

ES6 already has first-class support in modern runtimes.

Yes, and it does exactly what I said. When you compile your ES6 class to ES5, the resulting code is pretty much what the browser does with a non-compiled ES6 class. Quoting MDN:

JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript's existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript.

What modern browsers offer is a way to alter the constructor's prototype without actually assigning to it in your code. It's just a syntactic trick.

The whole point of ES6 classes is to stop the dichotomy of "one constructor and one prototype"

No, their point is to improve the syntax of working with "one constructor and one prototype."

An instance of a class that implements an interface with static properties would not have those properties

Yes, but we were talking about a hypothetical obj which is an object that is Widget-like, not a Widget instance. If you cram both static and instance members in one place, you can't use them separately:

let widgetLike = {}
widgetLike.x = 'foo' // should error type '"foo"' is not assignable to type 'number'

let widgetConstructorLike = {}
widgetConstructorLike.create = 42 // should error type '42' is not assignable to type '() => Widget'

Could you show how you would implement the types of widgetLike and widgetConstructorLike with an interface like this:

interface Widget {
  static create(): Widget;
  x: number;
}

How do you tell TS that widgetLike should have instance properties, while widgetConstructorLike should have only static properties? You can't do it like this:

let widgetLike: Widget = {}
let widgetConstructorLike: Widget = {}

...because those are identical types. Separating the two sides solves this problem:

interface Widget {
  x: number
}

interface WidgetCtor {
  create(): Widget
}

let widgetLike: Widget = {}
widgetLike.x = 'foo' // error

let widgetConstructorLike: WidgetCtor = {}
widgetConstructorLike.create = 42 // error

The cost of your solution is adding a new keyword for "implements static" or similar, which introduces the ambiguities discussed in comment 3 above.

Actually, it doesn't introduce ambiguities. I simply misinterpreted Ryan's comment. The static keyword would be a modifier to a member of the implements list. It simply denotes that the implemented interface should be part of the static side. Example:

interface Foo { f: number }
interface Bar { b: string }

// Foo and Bar are forced on the instance
class C implements Foo, Bar {
    f = 42
    b = 'hello'
}

// Foo and Bar are forced on the constructor
class C implements static Foo, static Bar {
    static f = 42
    static b = 'hello'
}

To me, that's as clear as day.

thw0rted commented 5 years ago

their point is to improve the syntax of working with "one constructor and one prototype."

OK, I definitely just assumed what "their point" is and have no particular information to back it up. If thinking of JS new-ables as having two distinct pieces is the right mental model, then so be it.

You can't do it like this:

let widgetLike: Widget = {}
let widgetConstructorLike: Widget = {}

See, to my eye that seems entirely obvious -- of course you can't just call them both Widget. I think we're arguing over a pretty minor point-of-view issue. I see a single dual-nature declaration as more natural, because classes are already dual-nature, and you see two single-purpose declarations as more natural because that allows for more flexible use.

That's what led me to the suggestion I made in my second comment this morning, addressed to Ryan, about how it would be nice to have a "magic" StaticShape conditional type. But as I say, I don't know how to make it, or if it's currently possible, or if it's even feasible to eventually make possible. And it would certainly be more complex for this use case (i.e. referencing the shape of the constructor function) than simply using two different interfaces in the first place.

hdodov commented 5 years ago

OK, I definitely just assumed what "their point" is and have no particular information to back it up.

You should try to avoid that.

I see a single dual-nature declaration as more natural, because classes are already dual-nature

Yes, but classes are dual-nature because their sole purpose is to ease the developer in defining the constructor interface and the interface of the object it creates. TypeScript isn't interested in describing classes (as it should), it's interested in describing those two interfaces. This makes sense because classes are just a syntax tool that allows the developer to define everything in one code block, while the constructor and the instance contain the actual logic.

you see two single-purpose declarations as more natural because that allows for more flexible use

We can just agree to disagree, but the TS developers have to make a choice. Flexibility means handling more use cases, which in turn means TypeScript is more useful. Isn't this more valuable than saving a couple lines of code that never make it to production anyway?

RaoulSchaffranek commented 4 years ago

I came up with two more use cases that also cover generics and inheritance and revealed some more open questions. I was trying to encode some of Haskells monad-instances, to see how typesafe I can get them with TypeScript.

Just for reference, here is a simplified version of Haskells monad type-class:

class Monad m where
  pure :: a -> m a
  bind :: m a -> (a -> m b) -> m b

Naively, I started with:

interface Monad<a> {
  pure (x: a) : Monad<a>
  bind (f: (x:a) => Monad<b>) : Monad<b>
}

The pure-method is similar to a constructor; it should be static. So, I tried to split the interface into its static and dynamic parts:

interface Monad<a> {
  bind (f: (x:a) => Monad<b>) : Monad<b>
}
interface MonadStatic<a> {
  pure (x: a) : Monad<a>
}

Next, I tried to implement the Identity-monad.

class Identity<a> implements Monad<a> {
  private value : a

  constructor (value: a) {
    this.value = value
  }

  static pure<a> (x: a) : Monad<a>{
    return new Identity(x)
  }

  bind (f: (x: a) => Identity<b>) : Identity<b> {
    return f(this.value)
  }
}

What's still missing is the static interface instantiation. However, the trick from the handbook does not work here, because MonadStatic<a> is generic in a:

const Identity<a>:MonadStatic<a> = class implements Monad<a> { /*...*/ }

So here we are. With the syntax proposed above, I think it should be possible to fully instantiate the identity-monad:

class Identity<a> implements Monad<a>, static MonadStatic<a> { /*...*/ } 

However, the signature of pure in the interface and implementation is still different. This is because the type-variable a is bound differently: once by the interface-declaration and once by the method-implementation.


Next, I tried to encode the Maybe-monad. The challenge here is that a Maybe offers two constructors. I implemented these as two classes. This made sense because the bind-method also behaves differently on either construction. The pure-method, on the other hand, belongs to neither of those classes. I moved it into a common parent-class. Let's fast-forward to the final implementation:

abstract class Maybe<a> implements static MonadStatic<a> {

  static pure<a>(x: a) : Maybe<a> {
    return new Just(x)
  }

}

class Nothing<a> extends Maybe<a> implements Monad<a> {
  constructor () {
    super()
  }

  public bind (f : (x : a) => Maybe<b>) : Nothing<b> {
    return new Nothing()
  }
}

class Just<a> extends Maybe<a> implements Monad<a> {
  private value : a

  constructor (value : a) {
    super()
    this.value = value
  }

  public bind(f : (x : a) => Maybe<b>) : Maybe<b> {
    return f(this.value)
  }
}

I think the separation of static and dynamic interfaces resolves rather elegant in this example.

However, that raises the question if child-class-constructors should inherit the properties of their parent's constructors. In my example, exposing Just.pure as a public interface would be undesired.

vzaidman commented 4 years ago
declare namespace React {
  namespace Component {
    const whyDidYouRender: WhyDidYouRender.WhyDidYouRenderComponentMember;
  }
}

worked for me somehow. see: https://github.com/welldone-software/why-did-you-render/blob/6a85ed215279840d0eedbea5d86eba92cfb1291b/types.d.ts

karol-majewski commented 4 years ago

@vzaidman's suggestion seems to be the only working workaround. However, it will only work in scenarios such as this one: the static field does not depend on the type of Props or State and it's optional.

Having an equivalent of implements applied to static fields would come in handy in other scenarios:

The static field is generic

The getDerivedStateFromProps React used by React is static, but it does need the type arguments provided when MyComponent is defined.

class MyComponent extends React.Component<Props, State> {
  static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): Partial<State> | null { /* ... */ }
}

Currently, getDerivedStateFromProps can be of any type. This is prone to human errors.

The static field is mandatory

Imagine a server-side rendering architecture in which a component tells the server how its data dependencies should be obtained by using a static method.

import { getInitialData, withInitialData } from "another-react-library";

class Page extends React.Component<Props, State> {
  static async getInitialData(props: Props) { /* ... */ }
}

export default withInitialData(Page);

If we wanted to make the getInitialData field mandatory so that each component can be server-side rendered, there is no way to achieve that right now. I could imagine having a class that requires it.

interface SSR<P, D> {
  getInitialData(props: P): Promise<D>
}

class UniversalComponent<P, S, D> extends React.Component<P, S> implements static SSR<P, D> { /* ... */ }

Such a component would be forced to define what it needs in order to be server-side rendered.

class Page extends UniversalComponent<Props, State, Dependencies> {
  // Required now!
  async static getInitialData(props: Props): Promise<Dependencies> { /* ... */ }
}
minecrawler commented 4 years ago

Here's my two cents:

I wish I could add static methods to my interfaces. The interfaces describe the API of an object. At the moment, that seems to be an instantiated class, however, when writing the interfaces and classes, we actually implement interfaces on classes, which are more akin to a messy mix of constructor and prorotype. A) Being able to write static methods inside a class alone is proof of that we cannot separate the two when working with class sugar.

From the docs:

In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

B) I think that means that an interface is the description of the capabilities a class provides, which may include static methods - which we cannot declare with today's interfaces.

So, if we use A) and B) as a line of thinking, there's a mismatch. Which is why I would like to suggest a re-alignment between what classes provide and what interfaces provide - in the most intuitive way. Just let an interface look like a class declaration, with the class being the definition. It's something which was possible way back in other languages (C++) as well, and helps have a clear contract.

interface IA {
  static x: string
  n: number

  static foo(): void
  bar(): void
}

class A implements IA {
  static x: string = 'Hello';
  n: number = 42;

  static foo(): void {}
  bar(): void {}
}

Also, let's not forget that it is valid code to pass a constructor as parameter, which allows us to do quite a bit of useful stuff and might benefits a lot from having an interface with static declarations. That's also something which has to be included in such a contract.

function foo<T extends Object>(obj: { new(): T }, ...args: any[]) {
  const instance: T = new (obj.prototype.constructor.bind(obj, ...Array.from(arguments).slice(1)))();
  const constructor = instance.constructor;
  const typeName = constructor.name;

  // ...
}

There were some doubts about this suggestion, so let me address them:

// arg has... no members?

Yes, if there are no non-static methods declared then arg has no members. If that situation makes sense should, however, not be part of a discussion about a language's capabilities, since it leads to exactly this kind of highly hypothetical situation, which cannot be assessed at a language level. Maybe it does make sense for the logic? Maybe it increases the developer experience? No one can make any good argument without a concrete case, and then we are talking about a concrete case instead of the language, so let's not talk about it here.

// How do I get to ctor(arg).x ?

arg.constructor.x, which is valid code, and I needed it in several projects, which heavily make use of the type system to create a sound library usage (take a look at this project, for example)

// Is this a legal call? // There doesn't seem to be any reason to reject it, // since 'fn' can't illegally access anything that doesn't exist fn({ });

No, it's not a legal call. {} does not implement the interface, so the type checker should reject it. fn cannot access arg.constructor.x, which is valid JS and valid TS, so there would be an obvious bug in TS if the above was allowed.

Oaphi commented 3 years ago

Another 2 cents (I apologize if that case was already discussed, at this point the discussion is too lengthy to be sure):

Being able to define static methods on an interface would also be useful when one tries to augment typings for a class they do not have control of, for example, an external library. For example, the declaration file of the library can have the following:

declare namespace A {
    class B { static init(opts: object) : B }
}

If one wants to add an override for the static init method in their code, it seems intuitive to use declaration merging. With interface this is currently not possible:

declare namespace A {
  interface B {
    //? Static members are not allowed here, and you can't merge classes
  }
  namespace B {
    //function init(kind: number): A; won't work as this is not the same as overloading
  }
}

This could be solved elegantly if static members could be defined on interfaces:

declare namespace A {
  interface B {
    static init(opts: { start ?: Date }) : B; //'static' modifier cannot appear on a type member
  }
}
snowyu commented 3 years ago

How to work with typescript in the following scenarios? Is there any solution or workaround?

  1. I had written an AOP framework to add AOP feature(I call it ability) into a class.

    const createAbility = require('custom-ability')
    class MyFeature {
      static coreAbilityClassMethod(){};
      coreAbilityMethod(){};
      additionalAbilityMethod(){};
    }
    
    const addFeatureTo = createAbility(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']);
    
    class MyClass {
      someMethod() {}
    }
    // inject the static and instance methods to the MyClass.
    addFeatureTo(MyClass);
    MyClass.coreAbilityClassMethod();
    const instance = new MyClass;
    instance.coreAbilityMethod();
  2. Dynamic inheritance and mixin library to inherits or mixin a class on run-time.

    const inherits = require('inherits-ex/lib/inherits')
    const mixin = require('inherits-ex/lib/mixin')
    const isInheritedFrom = require('inherits-ex/lib/isInheritedFrom')
    class Root {}
    class A {}
    class B {}
    class MyClass {}
    inherits(MyClass, [B, A, Root])
    assert.not.ok(inherits(MyClass, Root)) // inheritance duplication
    assert.ok(isInheritedFrom(MyClass, Root))
    
    class Mixin1 {
      static sm1() {}
      m1() {}
    }
    class Mixin2 {
      static sm2() {}
      m2() {}
    }
    // clone all members of Mixin1 and Mixin2 to MyClass
    mixin(MyClass, [Mixin1, Mixin2])
    MyClass.sm1()
    MyClass.sm2()
olee commented 3 years ago

I would be very interested to see progress on this proposal and I think the implements static X syntax would be a great addition to the language.

Therefore could someone maybe point out which questions / concerns are still open regarding this proposal or if it might be time to try implementing a PR for this feature? (not that I could do it ... sadly I have no knowledge of the TS compiler internals yet)

KutnerUri commented 3 years ago

I love the implements static X solution, a bit of a mind bender, but makes perfect sense:

interface JsonFactory<T> {
  fromJson: (obj: JSON) =>  T;
}

class Book implements Item, static JsonFactory<Book> {
  static fromJson(obj: JSON) { return new Book(...); }
  get ItemName() { return `book titled: ${this.name}`; } 
}

const books: Book[] =  [Book.fromJson({...}), Book.fromJson({...})];
const factories: JsonFactory<Item> = [Book, Pencil, Bookmark];

This is already great.

KutnerUri commented 3 years ago

I wish we could make it in the same declaration, like so

interface JsonFactory<T> {
  static fromJson: (obj: JSON) =>  T;
  toJson: () => JSON;
}

// and then use it like this?
function makeItem(factory: static JsonFactory<Item>, rawItem: JSON) {
  // ๐Ÿ‘Œ  just fine
  const item: Item = factory.fromJson(rawItem);
  const jsonItem: JSON = item.toJson();

  // ๐Ÿ™…โ€โ™‚๏ธ  not possible
  const json: never = factory.toJson();
  const item2: never item.fromJson({...});

  return item;
}

This does not replace implement static X though, it's just nice to have.

olee commented 3 years ago

@KutnerUri there has already been a lot of discussion regarding static declaration on interfaces themselves and there are many issues with this which is why this solution was already declined. Just take a look at this comment from @RyanCavanaugh above

minecrawler commented 3 years ago

@olee I already showed in my comment that the issues in that post actually do not exist, which means putting statics in the interface will work.

KutnerUri commented 3 years ago

@olee - ok. Let's start with implements static X

shekhei commented 2 years ago

Are there anybody that's already on this? any help needed? I am interested in contributing.

shekhei commented 2 years ago

I wish we could make it in the same declaration, like so

interface JsonFactory<T> {
  static fromJson: (obj: JSON) =>  T;
  toJson: () => JSON;
}

// and then use it like this?
function makeItem(factory: static JsonFactory<Item>, rawItem: JSON) {
  // ๐Ÿ‘Œ  just fine
  const item: Item = factory.fromJson(rawItem);
  const jsonItem: JSON = item.toJson();

  // ๐Ÿ™…โ€โ™‚๏ธ  not possible
  const json: never = factory.toJson();
  const item2: never item.fromJson({...});

  return item;
}

This does not replace implement static X though, it's just nice to have.

Not sure how will the function be used though, would it be expecting

makeItem(JsonFactory)
// or
makeItem(new JsonFactory<...>())

I am wondering instead, if something like this is useful

// and we might then have to limit 'extend static' to just interfaces? not sure
function <T extends static JsonFactory>(t: T) {
   // how should we access the static interface here? should T now also contain the static side of the constraints?
   // for example now this is becomes valid and gets converted into the `T.fromJson()` turns into `t.constructor.fromJson()`?
   T.fromJson(...)
}
Rodentman87 commented 2 years ago

I know I'm a little late to the party here, but if the entire point of classes is syntactic sugar, then why are we throwing that away with interfaces, forcing them to be separate like this? Something that exists in my library includes both static and instance methods that need to exist on a class. It seems weird to require someone to both implements and implements static two separate interfaces if they're both inherently tied together.

In terms of getting the static type of an interface with static members, if I have an interface:

interface Serializable<T> {
  static deserialize: (serialized: string) =>  T;
  serialize: () => string;
}

It's actually quite trivial to get the static type if I wanted to use that as an argument of a function, I've actually done this several times.

function doThing(clazz: Serializable<{}>['constructor']) {
  // ...do stuff
}

This could be turned into a default generic type just like Parameters<T> and look something like this:

interface Serializable<T> {
  static deserialize: (serialized: string) =>  T;
  serialize: () => string;
}

function doThing(clazz: Static<Serializable<{}>>) {
  // ...do stuff
}

Currently the only way to get this to (somewhat) work is doing something like this:

interface SerializableStatic<T> {
  deserialize: (serialized: string) =>  T;
}

interface Serializable<T> {
  constructor: SerializableStatic<T>;
  serialize: () => string;
}

function doThing(clazz: Serializable<{}>['constructor']) {
  // ...do stuff
}

But doing this only type checks usage of a variables typed as Serializable and doesn't type check classes that actually attempt to implement it.

I just think that in terms of ease of development and readability it makes a whole lot more sense to have interfaces follow more closely how classes are modeled rather than creating this divide. It's not very often that I've needed statics on an interface, but every time I've (personally) needed them it's always been in association with other instance members where it makes no sense to ever implement just the static members or just the instance members. (Like the provided Serializable example here)

olee commented 2 years ago

@Rodentman87 if you read the whole conversation and check this comment from @RyanCavanaugh above, you will see that there are various issues with having static properties on interfaces. People keep bringing this up, but because of the fundamental flaws of that proposal it is a no-go.

@RyanCavanaugh about the static-implements proposal: Do you know if there are any concerns left or would it be possible to implement this feature?

Rodentman87 commented 2 years ago

@Rodentman87 if you read the whole conversation and check this comment from @RyanCavanaugh above, you will see that there are various issues with having static properties on interfaces.

People keep bringing this up, but because of the fundamental flaws of that proposal it is a no-go.

I did read the whole thread as well as all the other threads about this topic. My comment was addressing those exact issues that were mentioned there.

olee commented 2 years ago

Oh yeah I saw you using static properties in an interface, but I didn't notice that you were actually referring to the issues of doing that. Sorry

minecrawler commented 2 years ago

@olee I addressed all of @RyanCavanaugh 's issues in one of my comments above. If I am wrong, please correct me. So far, I do not see any problems holding this proposal back :)

MasoudShah commented 2 years ago

If this feature in any way would be implemented, can we use the generic parameters to call the static methods? For example a common usecase will be the createInstance method like the code below:

interface Animal {
  static createInstance: () => Animal;
  print: () => void;
}

class Tiger implements Animal {
  static createInstance = () => new Tiger();
  print = () => console.log('Hey I am tiger!');
}

class AnimalFactory {
  static createAnimal<T extends Animal>(): T {
    let animal: T = T.createInstance();
    return animal;
  }
}

...
let animal = AnimalFactory.createAnimal<Tiger>();
animal.print();
...
KutnerUri commented 2 years ago

Not sure how will the function be used though, would it be expecting

makeItem(JsonFactory)
// or
makeItem(new JsonFactory<...>())

yes, clearly it would be makeItem(JsonFactory, { ... }).

const instance = new JsonFactory();
const prototype = JsonFactory;
const factoryMethod = JsonFactory.fromJson; // "function"
const factoryInstanceMethod = instance.fromJson; // undefined

instance is prototype; // true
prototype is instance; // false

mixing static JsonFactory and JsonFactory instance does not make sense:

function makeItem(factory: JsonFactory | static JsonFactory, raw: JSON) {
  if("fromJson" in factory) // is the static
    return factory.fromJson(raw);
  if("fromJson" in Object.getProrotypeOf(factory)) {
    // Object.getPrototypeOf(new A()) !== A. I did not expect that.
    return Object.getProrotypeOf(factory).fromJson(raw); // will throw exception and type warning
  }
  throw new Error('this is impossible type-wise);
}

maybe the problem is that there is actually no way of getting to the static methods from the instance? I'm surprised I can't find a way to do this in Javascript. But we are not trying to type the instances, we are trying to type the class itself.

minecrawler commented 2 years ago

@KutnerUri

maybe the problem is that there is actually no way of getting to the static methods from the instance? I'm surprised I can't find a way to do this in Javascript. But we are not trying to type the instances, we are trying to type the class itself.

In JS, you can use the constructor property: image

KutnerUri commented 2 years ago

@minecrawler - ah interesting. The constructor is a property coming from the prototype, and seems to represent the static class. Maybe this is what missing for typescript? Because right now instance.constructor is always of type Function.

So the corrected code would be:

function makeItem(factory: JsonFactory | static JsonFactory, raw: JSON) {
  if("fromJson" in factory) // is the static
    return factory.fromJson(raw);

  // (else) is the instance
  const ctor = factory.constructor; // infer as JsonFactory;
  if ("fromJson" in ctor) { // always true
    return ctor.fromJson(raw);
  }

  throw new Error("this is impossible type-wise");
}
seansfkelley commented 1 year ago

I have no preference one way or the other, but would love to have this feature. As a workaround, I currently write a "compile time unit test" of sorts:

interface Statics {
  doStuff(): string;
}

class MyClass {
  static doStuff() {
    return 'string';
  }
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: Statics = MyClass;

It gets the job done, but it's kinda gross, between the stray variable, the linter complaining, and the after-the-fact-ness of assignability checking (rather than the up-frontness of an implements clause).

1EDExg0ffyXfTEqdIUAYNZGnCeajIxMWd2vaQeP commented 1 year ago

MyClass satisfies Statics; would be the preferred way of writing this.

thw0rted commented 1 year ago

MyClass satisfies Statics; would be the preferred way of writing this.

Uh, y'all, this actually works right now:

class A {
    static f(): number {
        return 1;
    }
}

class B {
    static g(): string {
        return "hi";
    }
}

interface HasF {
    f(): number;
};

A satisfies HasF;
B satisfies HasF; // Type 'typeof B' does not satisfy the expected type 'HasF'. Property 'f' is missing in type 'typeof B' but required in type 'HasF'.

So.... close the issue I guess? ๐ŸŽ‰

minecrawler commented 1 year ago

So.... close the issue I guess? ๐ŸŽ‰

Sounds like a nice workaround, I really want to play around with that now! However it looks like just that: a workaround. Not static declarations in an interface, as requested in the OP :(

thw0rted commented 1 year ago

The OP isn't actually about interfaces with static members, it's about assigning (the static side of) a class to a variable that's typed with an interface. That's pretty much exactly what satisfies gives you, from my example. It would be nice to be able to combine it on one line (class Child extends Parent implements SomeInterface satisfies StaticThing { ... }), but the current two-line version gets the job done.

1EDExg0ffyXfTEqdIUAYNZGnCeajIxMWd2vaQeP commented 1 year ago

since expr satisfies Type is an expression that evaluates to expr, the following also works, but looks a little strange.

const A = class {
    static f(): number {
        return 1;
    }
} satisfies HasF;
hdodov commented 1 year ago

@1EDExg0ffyXfTEqdIUAYNZGnCeajIxMWd2vaQeP (very unwieldy username) note that this requires you to store the class in a variable. As I've said in my OP, this is already possible:

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
      console.log("beep beep");
  }
}

The issue is about making this possible without unnecessary variable assignments. Your workaround, as you've said, is in fact a little strange, and the whole idea is to make this more intuitive and approachable, which it currently is not.

thw0rted commented 1 year ago

I just realized that my "working" example above leaves you with additional statements in the JS emit. Look at the output on that Playground link, you have "A;" and "B;" at the end. That also means you'll trigger "no standalone statements" linter rules.

Maybe we can just make class A {...} satisfies Foo legal and call it a day?

minecrawler commented 1 year ago

Maybe we can just make class A {...} satisfies Foo legal and call it a day?

It already is legal; see @1EDExg0ffyXfTEqdIUAYNZGnCeajIxMWd2vaQeP 's variant.

interface IAStatic {
    f(): number;
}

interface IA {
    a(): void;
}

const A = class implements IA {
    static f(): number {
        return 1;
    }

    a() {}
} satisfies IAStatic;

I played around with it. I'm not 100% happy with having two interfaces (one for static, one for instance), but being able to add proper types in a relatively simple manner is already very good.

thw0rted commented 1 year ago

Doesn't const A = class { ... } have different type-space meaning than class A { ... }? I learned to always prefer named classes over const assignments in this case, and I thought it was for a good reason -- though I couldn't tell you exactly why if you put me on the spot.

minecrawler commented 1 year ago

Unfortunately, I'm not an expert on something low-level theoretical like that. My guess would be that one is a class (constructor) and the other one is a reference to a class (constructor). From my experience, they work interchangeably in practice.

To be honest, when writing code, I don't care that much about such things... I'm more worried about contracts and APIs being well typed and architected, which is why missing ways to put statics into an binding contract interfaces (as a trivial way to define the contracts) has always been a problem for me.

For the fun of it, I put the code into Babel and had it be transpiled to ES5 (in order to strip the class sugar). Both versions (assigning to a variable and defining the name directly) produce the same code (assign to a variable). However, if I had to manually write ES5 JS, I'd probably also make the difference:

// ES6+
const A = class {
    static f() {
        return 1;
    }
};

class B {
    static f() {
        return 1;
    }
}

// ES5
var A = function() {};
A.f = function() { return 1; };

function B() {}
B.f = function() { return 1; };

var is its own kind of beast (hoisting monster??), but iirc functions also used to be a hoisting mess. Today, we only use block-scoped declarations, so I don't think we would see any difference between A and B, even when writing

const A = function() {};
A.f = function() { return 1; };

function B() {}
B.f = function() { return 1; };

Correct me if I'm wrong, though!

TheUnlocked commented 1 year ago

Doesn't const A = class { ... } have different type-space meaning than class A { ... }? I learned to always prefer named classes over const assignments in this case, and I thought it was for a good reason -- though I couldn't tell you exactly why if you put me on the spot.

const A = class { ... } will have A.name === '' rather than A.name === 'A' as intended. That can have consequences when debugging (e.g. React likes to leverage function and class names for their dev tools and when making error messages), so you would need to repeat the name for both the variable and the class expression.

yume-chan commented 1 year ago

I think #32452 is a better solution for this issue. With that implemented this should automatically type checks:

interface I {
    constructor: { fn(): void; }
}

class C implements I {
    static fn(): void { }
}

It doesn't require any new syntax, and works as real JavaScript does: (new C()).constructor.fn === C.fn.

Except it's unclear how to extend interfaces with static part, maybe it will need this:

interface IA {
    constructor: { a: number };
}

interface IB extends IA {
    constructor: IA["constructor"] & { b: string };
}