Skip to content

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

Closed
@hdodov

Description

@hdodov

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions