Typescript: static property inheritance complaining when it shouldn't

Created on 3 Sep 2015  路  10Comments  路  Source: microsoft/TypeScript

I've seen that you've already closed many issues on this. But I'm reporting another one in hopes to wear you down ;)

It seems I can't change the signature of a static function in a derived class. The fact that the subclass is at all aware of the static functions on the super class is really strange.

I developed in C# for years, so I know what you were _trying_ to do... but given that you don't support a new keyword like C# does, I think this behavior is really wrong. If you're looking to target being a superset of ES6/ES7, you need to correct this behavior ASAP. Generally in JavaScript, static properties are not copied to subclasses unless the copy is explicit.

I ran into this issue with RxJS Next, where I Subject must inherit from Observable, but they both need _different_ static create() signatures. So I end up hacking around and fighting TypeScript.

Committed Suggestion

Most helpful comment

So... and now there a plans to change this? I mean, this is pretty old now? Plans for a survey or something?

All 10 comments

The intuition we tried to match is that a specialized class can stand in for its base; which is a general intuition that an OOP developer would expect.

can you expand more on your scenario, and assuming a base class is needed but not expected to be generic enough to handle the child?

The issue is specifically where Observable.create and Subject.create need to have different signatures.

As you can see, I've found a workaround, but it's not ideal.

I guess ES6 does pull the static functions over. But I'm able to redefine them at will in ES6. TypeScript complains. It's problematic for functions that are idiomatic in many JavaScript libraries like static create functions.

Summary of discussion from the Slog today:

  • Static inheritance _is_ part of the ES6 spec, but...
  • Some people use the static side of classes (i.e. constructor functions with static properties) polymorphically, and some don't
  • The first group wants this check as much as they want general substitutability checks for the _instance_ side of classes
  • The second group doesn't care at all if their static members align or not
  • The first group _should_ have their substitutability failures detected at the use sites, which is worse than at declaration sites, but still (mostly) works
  • The second group is just out of luck in our current design

Conclusion: Try removing the check for assignability of the static side of classes and see what kind of errors go away (mostly in the baselines, since we mostly have clean code in our real-world-code baselines). Proceed depending on those results.

The Committed tag here is tentative.

So... and now there a plans to change this? I mean, this is pretty old now? Plans for a survey or something?

Static factory methods are a useful design paradigm, for instance when object creation relies on data that needs to be asynchronously fetched a static create() method can encapsulate the data fetching, and return the object type (as such fetches cannot be conducted directly in the constructor). This behaviour also aligns with the OOP principles in the popular object oriented languages C# and Java. Requiring that in inheritance each static method with the same name must have compatible types restricts this significantly (for create(), this would enforce the use of the same parameters). Is there an argument for maintaining this restriction?

For example, consider the following:

class Entity {
  id: string;
  name: string;
  constructor(id: string, name: string) {
    this.name = name;
    this.id  = id;
  }

  static async create = (name: string): Promise<Entity> => {
    const { id } = await db.createEntity(name);
    return new Entity(id, name);
  }
}

class Bob extends Entity {
  age: number;
  constructor(id: string, age: number) {
    super(id, 'Bob');
    this.age = age;
  }

  // TS Error: Types of property 'create' are incompatible
  static async create = (age: number): Promise<Bob> => {
    const { id } = await db.createBob(age);
    return new Bob(id, age);
  }
}

I agree that this should at least be a strictness option that can be turned off. In some circumstances people might actually use constructor values and their static methods polymorphically, but I don't think that's a widely understood expectation, and this restriction gets in the way of entirely reasonable code such as static constructor methods with the same name but different parameter types.

Recently ran into this as well.

type Class<T> = {
    readonly prototype: T;
    new(...args: any[]): T;
};

class Foo {
    public static create<TFoo extends Foo = Foo>(this: Class<TFoo>, model: Partial<TFoo>): TFoo {
        return new this(model.propertyA);
    }

    public constructor(public propertyA?: string) { }
}

class Bar extends Foo {

    public static create<TBar extends Bar = Bar>(this: Class<TBar>,model: Partial<TBar>): TBar {
        return new this(
            model.propertyB,
            model.propertyA,
        );
    }

    public constructor(
        public propertyB: number,
        public propertyA?: string,
    ) {
        super(propertyA);
    }
}

Intuitively speaking, as Bar#create breaks the prototypal "link", I find this restriction bit of an annoyance. Refer this playground link.

Contextually, I also tried to use the InstanceType utility class to find a workaround. But w/o much luck. Here is the playground link for that.

Due to the lack of better example, I would like to point out that with C#, similar behavior is possible (just to justify the pattern/example).

```c#
using System;

namespace trash_console
{
class Program
{
static void Main(string[] args)
{
Bar.Create();
Foo.Create();
Console.ReadLine();

  // output:
  //
  // Foo ctor
  // Bar ctor
  // Foo ctor
}

}

internal class Foo
{
public static Foo Create()
{
return new Foo();
}

public Foo()
{
  Console.WriteLine("Foo ctor");
}

}

internal class Bar : Foo
{
public new static Bar Create()
{
return new Bar();
}

public Bar()
{
  Console.WriteLine("Bar ctor");
}

}
}
```

This is causing a number of problems trying to migrate Closure Library to TypeScript. In particular, many of our legacy classes make common use of the pattern

class Component {}
namespace Component {
  export enum EventType { FOO }
}

class Button extends Component {} // TS2417
namespace Button {
  export enum EventType { BAR }
}

But this complains about class-side inheritance since the two EventType enums are incompatible.

In actuality, subclass constructors are not generally substitutable for one another, so enforcing it at the class level seems misguided. In particular, all the existing enforcement does nothing to warn about the following broken code:

class Component {
  private readonly x = 1;
  constructor(readonly componentType: string) {}

  static create(): Component {
    return new this('generic');
  }
}

class Button extends Component {
  private readonly y = 2;
  constructor(readonly size: number) {
    super('button');
    if (typeof size !== 'number') throw Error('oops!');
  }
}

Button.create(); // NOTE: no error, but throws

The correct solution would be to to (ideally) ban unannotated use of this in static methods of (non-final) classes and require explicit specification:

class Component {
  static create<T extends typeof Component>(this: T) { ... }
}

Button.create(); // Error: Button not assignable to typeof Component

This triggers an error at the call site of Button.create() since TypeScript can already tell that Button is not assignable to typeof Component. Given this, it makes sense to loosen up on the requirement that overridden static properties must be compatible. TypeScript's structural typing is smart enough to know when it matters.

EDIT: clarified the code examples a little

The trouble is that inheritance of static methods works differently from inheritance of instance methods. Here is a simplified example based on a real world problem I've encountered. GObject has this static method:

static install_property(property_id: number, pspec: ParamSpec): void

GtkSettings is derived from GObject and has this static method:

static install_property(pspec: ParamSpec): void

If they were instance methods, yes that would cause problems whether you were using JS or TS, but as they're static methods you're always going to qualify them when you call them eg GtkSettings.install_property(pspec), so there shouldn't be any confusion over which method gets invoked. I know there can be problems when using this in static methods, but that isn't an issue here because these are bindings for native code.

However, the only way I can declare Settings' method in Typescript is by tricking it into thinking it's an overloaded method ie add declarations for both signatures to Gtk.Settings. This is the exact opposite of what Typescript is supposed to do in terms of type safety etc!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

remojansen picture remojansen  路  3Comments

weswigham picture weswigham  路  3Comments

manekinekko picture manekinekko  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

dlaberge picture dlaberge  路  3Comments