TypeScript: static property inheritance complaining when it shouldn't
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.
About this issue
- Original URL
- State: open
- Created 9 years ago
- Reactions: 31
- Comments: 28 (8 by maintainers)
Commits related to this issue
- docs: mark args as any This is also due to https://github.com/microsoft/TypeScript/issues/4628 which prevents changing the signature of static methods on inherited classes. — committed to pyoor/webidl2.js by pyoor a year ago
- docs: mark args as any This is also due to https://github.com/microsoft/TypeScript/issues/4628 which prevents changing the signature of static methods on inherited classes. — committed to pyoor/webidl2.js by pyoor a year ago
- feat: allow adding additional members to production parse methods (#745) * feat: allow adding additional members to production parse methods * feat: add 'extensions' option for extending existing... — committed to w3c/webidl2.js by pyoor a year ago
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 (forcreate(), this would enforce the use of the same parameters). Is there an argument for maintaining this restriction?For example, consider the following:
How about a new keyword
overridein the child class, that would explicitly tell the compiler to fuck off?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:
Example usage:
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.
Base’sstatic foo()callsthis.bar(42)butSubincompatibly overridesstatic bar(arg: string)). If you want that extra protection, you could take a broader approach by eliminating all statics from the base class (this has the added “benefit” of enforcing that statics are only called directly on the classes they’re defined on, which I’d consider best practice as long as you’re not relying on the class-side dynamism):Playground examples
Recently ran into this as well.
Intuitively speaking, as
Bar#createbreaks the prototypal “link”, I find this restriction bit of an annoyance. Refer this playground link.Contextually, I also tried to use the
InstanceTypeutility 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).
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.
Please, we need an update on this
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
FooBar.factory=FooBar.FooBarFactory. However, even this sucks. If I wanted the factory to have a short name, for example having aResultstatic factory on anErrortype to wrap them in aResulttype, then you’re all out of luck. There are for sure other valid examples where shorter and consistent names are desired.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.
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
// @ts-expect-errorwithout having to suppress errors on the entire class.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.
@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
Base<T>. But trying to figure out something.Yes, this is the same issue. The declared type of
Sub.bulkCreateis not a subtype of the declared type ofBase.bulkCreate. TypeScript is expecting the static methods to follow Liskov substitution, so that you could writeBut as you’ve written your
Sub.bulkCreatesignature, you’ve constrained the input typeMto be narrower in the subclass than in the base class (it requiresthisto beSub<ExtendedEntity>, rather than aBase<BaseEntity>). This is somewhat confusing for two reasons: (1) because function parameters are contravariant, so(arg: number) => voidis actually a supertype of(arg: unknown) => void, even thoughnumberis a subtype ofunknown. But also (2) TypeScript plays a little fast and loose with this rule by allowing methods (i.e. using method shorthand rather than a property with a colon) to break this rule, so a lot of people get away with breaking it without realizing what they’re doing.The reason the error goes away when you remove
doSomethingis because TypeScript types classes structurally: so without that method the template parameter no longer shows up anywhere in the type, so thatSub<A> === Base<B>for allAandB, as far as the type checker is concerned (andSubdoesn’t add any other properties, either). Removing the extra property fromExtendedEntitywould also fix the error, since as you’ve written itSub<A> === Base<A>already.Back to the original issue, there should be nothing wrong with what you’ve written. The type checker can see that
typeof Subis not assignable totypeof Base, and can enforce this just fine. If you wanted to make it assignable, you can, but it should let you choose to have it not be assignable (which it’s not). You can use the workaround I posted earlier to removebulkCreatefromBasein theextendsclause, but we shouldn’t have to do that.It’s perhaps worth noting that
constructoris already exempt from substitutability checks, and in JS the constructor function is the class, which means in practice there’s a good chance a subclass can’t stand in for its parent class anyway and at that point enforcing that all thestaticmembers of the subclass are compatible with the ones in the base class doesn’t actually accomplish much.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
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
getP(C)will typecheck just fine becausetypeof Cis a subtype oftypeof A, but this is not just becauseCis a subtype ofA(subtypeness is not invariant acrosstypeof, though that’s a partial requirement): it’s also necessary that the static side be completely compatible. That’s when the type checker should enforce the static side types. And ifD extends Ais never passed astypeof Athen it can definepto be whatever type it wants. (For subtyping compatibility, it’s also necessary that the constructor arguments be compatible; if that’s not needed for some application, a structural type or interface like{p: string|string[]}could be more broadly useful.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:
GtkSettings is derived from GObject and has this static method:
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 usingthisin 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!
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
But this complains about class-side inheritance since the two
EventTypeenums 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:
The correct solution would be to to (ideally) ban unannotated use of
thisin static methods of (non-final) classes and require explicit specification:This triggers an error at the call site of
Button.create()since TypeScript can already tell thatButtonis not assignable totypeof 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