Skip to content

Suggestion for specifying a class static side interface that supports derived classes and type parameters #33916

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

Closed
5 tasks done
hdodov opened this issue Oct 10, 2019 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@hdodov
Copy link

hdodov commented Oct 10, 2019

Search Terms

class static side interface extend

Problem

Currently, there's no way to set the static side interface of a class. This is something dating back to #1263 and has been a topic more recently in #17545.

Some suggest adding static properties on interfaces. That doesn't solve the problem in a reasonable manner, I think. Other suggestions propose defining the class with an expression, but that doesn't completely solve the problem either. Consider this:

interface StaticSide {
    len: number
}

let myClass: StaticSide = class Foo {
    static len = 42
}

I can enforce that Foo has the static len. But I want to extend that class too, and the derived classes should have this property as well. Currently, this can't happen:

// Type 'StaticSide' is not a constructor function type.
class Bar extends myClass {}

// Cannot find name 'Foo'.
class Bar2 extends Foo {}

You might suggest to simply put the len property in the class itself and not use an interface in the first place:

class Foo {
    static len = 42
}

// Type 'string' is not assignable to type 'number'.
class Bar extends Foo {
    static len = 'bar'
}

In this case, that would work, but suppose we have a more advanced use case with type parameters:

interface StaticSide<T> {
    len: T
}

let myClass: StaticSide<number> = class Foo {
    static len = 42
}

Now, you are forced to use a class expression because defining the static property as earlier is not possible:

// Static members cannot reference class type parameters.
class Foo<T> {
    static len: T
}

class Bar extends Foo<number> {
    static len = 'bar'
}

This error makes sense because the len in Foo can be of any type, and its implementation probably expects a certain type. As said in #33870.

Suggestion

We need some sort of way to attach a static side interface to the class itself, not just the resulting variable of an expression. I believe this can be solved with the suggestion in #33892:

interface StaticSide<T> {
    len: T
}

class Foo: StaticSide<number> {
    static len = 42
}

class Bar extends Foo {
    // Type 'string' is not assignable to type 'number'.
    static len = 'bar'
}

class Bar2: StaticSide<string> extends Foo {
    // Type 'number' is not assignable to type 'string'.
    static len = 64
}

As demonstrated, the specified static type in Foo now has a type parameter, but it is used as a value. Unlike the problem earlier, len in Foo now has a determined type, number.

Derived classes of Foo should have the static side of Foo enforced on them, requiring len of Bar to be a number. However, if the derived class declares its own static side, it is merged on top of the parent's static side. That's why len in Bar2 is expected to be string.

Examples

Basically, to set the instance side of a class, you use implements. To set the static side, you assign a type to its symbol:

interface StaticSide<T> {
    len: T
}

interface InstanceSide<Y> {
    name: Y
}

class Foo: StaticSide<number> implements InstanceSide<string> {
    static len = 42
    name = 'foo'
}

Use Cases

Essentially, this solves three issues:

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@hdodov
Copy link
Author

hdodov commented Oct 11, 2019

This could also help with issues like #32452 that are related to this.constructor. If the class has a well defined static side, then constructor on the instance side can be the merge of Function and the static side. For example:

interface StaticSide {
  len: number
}

class Foo: StaticSide {
  static len = 42
}

let inst = new Foo()
inst.constructor.len = 'foo' // Type 'string' is not assignable to type 'number'.

In this case, the type of inst.constructor should be Function & StaticSide.

@RyanCavanaugh
Copy link
Member

I'm unclear on the difference between this and #33892 ?

@hdodov
Copy link
Author

hdodov commented Oct 18, 2019

@RyanCavanaugh in #33892 I wanted to propose just the syntax change. In this issue, I wanted to propose functionality with that syntax change as proposed implementation.

If you think my suggestion here overlaps with what you have in mind there (except the syntax), then close this.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Oct 18, 2019
@RyanCavanaugh
Copy link
Member

Let's track it all in the other issue. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants