-
Notifications
You must be signed in to change notification settings - Fork 12.8k
static property inheritance complaining when it shouldn't #4628
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
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 |
Summary of discussion from the Slog today:
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 |
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 For example, consider the following:
|
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 Contextually, I also tried to use the 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). 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 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 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 EDIT: clarified the code examples a little |
Class static side 'typeof Maybelist' incorrectly extends base class static side 'typeof Monad'.
It is not clear to me why the error is on the class not on the static method I would like to disable this error I have other static methods in the same class that would benefit from this type check so I would like to be able to disable it at the static method level instead of the class level or the project level but anything will be appreciated since it will fix a 5 year old issue (#39699) Left pane: public static fromValueOf in class Maybelist uses generic xTVal line 24 // Line 24 in class Maybelist<Val = unknown, MLVal extends Array<Val> = Val[ ]> extends Monad<MLVal> :
public static fromValueOf<xTVal>(value: Functor<xTVal[]>): Maybelist<xTVal> {
return Maybelist.of<xTVal>(
(Monad.from<xTVal[]>(value.clone).fork as unknown) as xTVal,
);
}
/* [...] */
// could have been simplified like this:
public static fromValueOf<xTVal>(value: Functor<xTVal[]>): Maybelist<xTVal> {
return Maybelist.of<xTVal>(value.clone);
}
/* [...] */ Right pane: public static fromValueOf in class Monad uses generic yTVal line 15 // Line 15 in class Monad<MVal = unknown> extends Chain<MVal> implements IMonad<MVal> :
public static fromValueOf<yTVal>(value: Functor<yTVal>): Monad<yTVal> {
return Monad.from<yTVal>(value.clone);
}
/* [...] */ PlaygroundLink to playground simplified version
I don't know what @sandersn will think of that... I am working on this thing for too long... probably I should just switch back to JavaScript (People who know me know I am addicted to TypeScript and I won't go back to JavaScript) |
@realh: "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 This is not always true. My team has a lot of code where static members are the implementation of a static interface that is accessed generically on constructor instances. This is a simple example of configuration statics that cause problems because static declarations are allowed to narrow the static type of a supertype. Once the type is narrowed, a further subclass can't widen it again without a cast: class A {
static p: string | string[];
}
class B extends A {
static p = 'B';
}
// Error
// Class static side 'typeof C' incorrectly extends base class static side 'typeof B'.
// Types of property 'p' are incompatible.
// Type 'string[]' is not assignable to type 'string'.(2417)
class C extends B {
static p = ['B'];
}
function getP(ctor: typeof A): string {
return typeof ctor.p === 'string' ? ctor.p : ctor.p.join(',');
} What I think I want here is that without an explicit type annotation, |
I'd argue that the extends-time enforcement is the fundamental problem. The check ultimately needs to be at the usage site. In your example, the call to |
Yeah, this seems fatally broken to me, especially since it applies to private statics as well. And I can see no advantage to the current behavior that could possibly outweigh the loss of the factory method pattern. |
There's also unfortunately no remotely reasonable way to work around this. Any suppression has to happen at the level of the entire class, which loses a lot of type checking. |
Any update on this? i'm stuck, i want to have different parameters on parent static function and in child static function, i don't see necessary that Typescript does this with static functions |
Please, we need an update on this |
At the very least, is there any chance we can get the error to appear at the static method level, as opposed to the class? This would allow us to suppress it with |
FWIW: I'm going to stop watching this, but I'll leave it open for others. I'm okay with the current behavior because I try to avoid inheritance like the plague now. ... and static methods, TBH. haha. Partially because of this issue. |
I still believe this is worth fixing (see my earlier comments), but I've come up with a free-after-typechecking workaround that at least unblocks me, so I thought I'd share it here: type OmitStatics<T, S extends string> =
T extends {new(...args: infer A): infer R} ?
{new(...args: A): R}&Omit<T, S> :
Omit<T, S>; Example usage: class Base {}
namespace Base {
export enum EventType { A }
}
class Sub extends (Base as OmitStatics<typeof Base, 'EventType'>) {}
namespace Sub {
export enum EventType { B }
} TypeScript fully understands the correct class relationship, but it doesn't complain about the otherwise-incompatible override anymore. Warning: nothing prevents you from calling an incompatibly-overridden static method unsafely (e.g. type OmitAllStatics<T extends {new(...args: any[]): any, prototype: any}> =
T extends {new(...args: infer A): infer R, prototype: infer P} ?
{new(...args: A): R, prototype: P} :
never;
class Sub extends (Base as OmitAllStatics<typeof Base>) { /* ... */ } |
In functional type theory, a type is defined as a set of object along with a set of operation. It's analogous to an algebraic structure except it doesn't have to comply with closure axiom. The operations above may have the type as both its input or output. However, in TypeScript, method call is written as For example, there is a type A, which have 2 operations:
capture :: A -> Snapshot
restore :: Snapshot -> A In TypeScript, you can capture like class A {
public capture(): Snapshot;
public static restore(s: Snapshot): A;
}
const a = A.restore(snapshot); Therefore, the semantic meaning of static methods of a class is the algebra-like operations as well. This is why many non-functional OOP language all deicide to inherit static members automatically. |
It's perhaps worth noting that |
Is the following the same or a bug?
Strange, as if I remove
|
Yes, this is the same issue. The declared type of
But as you've written your The reason the error goes away when you remove Back to the original issue, there should be nothing wrong with what you've written. The type checker can see that |
@shicks reading your workaround again after this explanation just really made it super clear, I can't upvote this enough. I switch would be nice to to disable this check(or make it appear on the function and just ignore it there), but I really think this could work well for more complex cases we use. Thanks! *update: Still have an issue with generic classes as now the generic properties simply grabbed from Base, not |
This is also due to microsoft/TypeScript#4628 which prevents changing the signature of static methods on inherited classes.
This is also due to microsoft/TypeScript#4628 which prevents changing the signature of static methods on inherited classes.
) * feat: allow adding additional members to production parse methods * feat: add 'extensions' option for extending existing productions * fix: remove unnecessary spread operator Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com> * refactor: rename extension 'callback-interface' to callbackInterface * test: improve extension parsing tests * docs: fix up jsdoc definition for ParserOptions * test: remove use strict * test: merge extension test into custom-production * test: replace customProduction with top-level CustomAttribute * test: remove extension argument from collection utility * docs: normalize use of Token import * test: fix import of expect function * docs: mark args as any This is also due to microsoft/TypeScript#4628 which prevents changing the signature of static methods on inherited classes. * docs: fix path to container.js * refactor: remove unnecessary spread operator * docs: fix jsdoc types Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com> * docs: fix jsdoc types Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com> * fix: remove iheritance attribute from CallbackInterface --------- Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
How about a new keyword |
So from what I can gather, the reason this issue is dead in the water is because of these concerns. However, the inaction has come at the cost that you cannot share the name of your static factory methods. As I understand this exact same restriction isn't applied to constructors, as the issue would appear far more frequently. Which makes sense because it would make using constructors completely intolerable. Well this is an intolerable issue and you simply cannot use static factory methods that will clash. The type gymnast workarounds also all suck, and no one should reasonably incorporate them into their code. The workaround I wanted to use is probably something like this: pay for the pain in verbose names, use the class name again in your static factory methods such that What makes it acceptable for this issue to be parked? The hope that very few users come across it without quickly finding a suitable workaround, but there does seem to be a lot of confidence in that. Meanwhile I'll compromise too by using different names. |
Good point: "prefer composition over inheritance...". Hitting this issue in Typescript can help you decide in favor of composition (without necessarily completely avoiding inheritance for an entire codebase) |
…e name, but different signatures This is a weird restriction in TypeScript that other languages don't have (and JS would actually allow it). Seems that they decided to do it because the value of `this` can change, but we can't use `this` in statics in Haxe, so that doesn't really apply to us. Regardless, it should be possible to workaround by ommitting fields when extending a class, but I don't have time yet, so skip conflicts for now to allow Feathers UI to generate valid TS externs. Relevant link: microsoft/TypeScript#4628 (comment)
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.The text was updated successfully, but these errors were encountered: