You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Subclass method is not allowed (TS2425) if the parent class has a property of type any with the same name #27965
TypeScript appears to treat class methods as something fundamentally different from a class prototype property of type function. This is not in agreement with the standard semantics of ES6.
TypeScript Version: all versions, as far as I have been able to test.
Search Terms: method member property any parent class subclass
Expected behavior: should compile without a problem. Note that by the ES6 standard, the above definition of Derived is equivalent to the following, which TypeScript compiles without a problem:
Related Issues: I didn't find similar issues, although I started by looking for issues requesting for the ability to disable particular errors (TS2425 in this case). There are several (closed) issues in which such an ability is requested or suggested, but I believe the proper solution would be to bring TypeScript in agreement with ES6. After all, it claims to be a superset!
The problem here is that prop(){} defined as method is placed on the prototype. prop: any as property is written directly to the instance (if it had an initializer) and thus overrides the prototype method.
Consider the following slightly modified version of your code:
classSuper{prop: any=undefined;}classDerivedextendsSuper{prop(){}}newDerived().prop();// runtime error: undefined is not a function
You can still use a function to initialize prop in Derived:
classSuper{prop: any=undefined;}classDerivedextendsSuper{prop=()=>{}}newDerived().prop();// works as expected
Reacted by yukihirop, Federico Ferreyra, Ukonn Ra, RAMPAGE and Walter Perdan
Thanks Klaus Meinhardt (@ajafff), I didn't think of that. The reason I'm running into this is not resolved yet, though. A common pattern in the JavaScript world is to allow an object property to either be of type A or a bound method of type () => A. Underscore's and Lodash's _.result is specifically meant to cater to this scenario. However, there seems to be no way in TypeScript to express such a typing:
// first attemptclassSuper{prop: any;}classDerivedextendsSuper{prop(){}}// problem as above
// second attemptclassSuper{prop(){return{};};}classDerivedextendsSuper{prop={};}// Property 'prop' in type 'Derived' is not assignable to the same property in base type 'Super'.// Type '{}' is not assingable to type '() => {}'.// Type '{}' provides no match for the signature '(): {}'.
There may be a way I'm not aware of (I hope there is), but otherwise, I'd argue TypeScript is missing something.
This has come up in the past but the specific scenario of "this method may be overriden by a property of a different type" hasn't come up often enough to warrant a solution
Fair point. I started this ticket with different assumptions, so the opening post and the title probably don't accurately reflect anymore what this issue is really about.
I'm not meaning to imply that TypeScript should just accept the example code I presented. Rather, the problem is that there seems to be no alternative. Semantically, it is a valid and even meaningful construction in JavaScript to override a base class method with a subclass non-function prototype property or vice versa. This is especially the case if the return type of the method is the same as the type of the non-function. To me, this seems like a scenario that could be type-checked, but again, there seems no way to do this in TypeScript.
If it hasn't come up often, then apparently not many Backbone users work with TypeScript. In Backbone, _.result is used all over the place. You may be able to gain some ground there!
For completeness (and people seeking help with TS2425 errors), you can always use type merging to "tell" TS that the member shall be seen as method.
The examples above for instance can be solved like this:
interfaceSuper{test?(): void;}classSuper{test=function(){}// test may also be undefined, it's optional on the interface}classDeliveredextendsSuper{test(){}}
interfaceSuper{test?: boolean|(()=>boolean)// ERROR, All declarations of 'test' must have identical modifiers. (2687)}classSuper{// I want this to be protected!protectedtest// ERROR, All declarations of 'test' must have identical modifiers. (2687)}classDeliveredextendsSuper{// ERROR, Property 'test' is protected in type 'Delivered' but public in type 'Super'. (2415)protectedtest=true}
This is a pattern I've seen in base classes before (in plain JS).
For example, here is a more representative example of what a base class may want to do (note the "ERROR 2425"):
import*asReactfrom'react'// this is only for JSX types, but we're not using React in the following example...typeTemplate=Element|(()=>Element)interfaceSuper{template?: Template}classSuperextendsHTMLElement{// Subclasses can optionally provide a `template`, writing JSX to create a DOM tree.// BUT! Not all elements need to have their own Shadow DOM tree, so they don't have to define this.protectedtemplateconnectedCallback(){consttmpl=this.templateif(tmpl)this.render(typeoftmpl==='function' ? tmpl : ()=>tmpl,this)}privaterender(template: ()=>Element,ctx: this){this.attachShadow({mode: 'open'})this.shadowRoot!.append(template.call(this))}}// This element has a simple static Shadow DOM, written with concise syntax.classSomeElementextendsSuper{protectedtemplate=div(<div>This should work.</div>)}customElements.define('some-element',SomeElement)// This element needs no inner Shadow DOM.classOtherElementextendsSuper{connectedCallback(){super.connectedCallback()// ...does something special, without needing a `template`...}}customElements.define('other-element',OtherElement)// This element make a local reactive variable (state) to render in its DOM. classAnotherElementextendsSuper{count=reactiveVar(0)protectedtemplate(){// <------- ERROR 2425 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!setInterval(()=>this.count.set(this.count.get()+1),1000)returndiv(<div>This should work too. The count is {this.count.get()}</div>)// updates the DOM every second.}}customElements.define('another-element',AnotherElement)declarefunctionreactiveVar<T>(initialValue: T): {get(): T;set(val: T): T}// This is simple a no-op identity function, to type cast JSX.Element to a DOM type.// See issues like// https://github.com/microsoft/TypeScript/issues/14729// https://github.com/microsoft/TypeScript/issues/31606// https://github.com/microsoft/TypeScript/issues/35929declarefunctiondiv(j: JSX.Element): HTMLDivElement
Now strip all the type from that example, and you'll see that it is valid and well-functioning JavaScript.
Some people mention that we can just use an arrow function, like
classDerivedextendsSuper{prop=()=>{}}
but this is not ideal because
it is wasteful to create one new function per instance, instead of sharing a single prototype function.
it does not work with inheritance (we can not use super in an arrow function to call the same function of a super class).
Ryan Cavanaugh (@RyanCavanaugh) It seems that a super class having an initializer is the main foot gun that the current restriction aims to prevent.
Can we perhaps have a way to allow overriding properties with prototype methods under the condition that the super class must not have an initializer for the property being overridden (and with strict types therefore requires it to be an optional property)?
classSuper{prop?: boolean|(()=>boolean)=false// This has an initializer. It could also be in the constructor.}classDerivedextendsSuper{prop=true// ok}classDerived2extendsSuper{prop(){returntrue}// error}
has a type error that says something like "The Super base class property prop has an initializer, therefore the subclass Derived2 can not override prop with a prototype method."
I think allowing this under certain conditions would be great!
But in as in #18654 says they should behave different in strict mode.
A method and a function property of the same type behave differently.
Methods are always bivariant in their argument, while function properties
are contravariant in their argument under strictFunctionTypes.
How can I do this if I want to use function properties typechecking here with extend class override here?
I came across this error when I wanted subclasses to be able to signal that the method does not exist by setting it to null.
What I want to be able to do
interfaceA{// if a subclass wants they can set this method to nullfoo: null|(()=>number);// property foo was also declared here}classA{foo(): number{return1;}// duplicate property}classBextendsA{}B.prototype.foo=null;
I can however change the definition for class A to:
classA{}A.prototype.foo=()=>1;// no problem - just as the first comment in this issue found
But since this is valid typescript I would have assumed the first version was also valid...
I can see how it's not. I guess it's because prototype is itself an instance of type A and so assigning foo to be a function is like defining an instance member property. Typescript considers it different to defining an instance member function inside the class body, even though it all ends up on the prototype anyway.
It would be nice if my original code worked, I don't mind the workaround but I don't love it.
Note the opposite is also problematic
interfaceA{foo: null|(()=>number);}classA{}A.prototype.foo=null;classBextendsA{foo(): number{return1;}// class A defines an instance member property but extended class B defines it as an instance member function}// B.prototype.foo = () => 1 // no problems
(I also want to avoid defining functions that end up on the instance and would prefer to keep them on the prototype).
I don't know who came up with this first, but TS and ESnext basically share the same critical mistake to consider a "class field"
classA{prop='abc';}
equivalent to a constructor initializer
functionA(){this.prop='abc';}
instead of a default property on the prototype.
functionA(){}A.prototype.prop='abc';
Besides the fact that this is inconsistent with member functions (which are set on the prototype), this causes all kinds of nasty problems. For example, you can't override a parent "class field" by setting a derived class prototype property.
If "class fields" were prototype properties instead, there wouldn't be any problem of this kind (below, I'm avoiding class field syntax in order to avoid confusion).
I think that the current issue ticket is a symptom of this problem being reflected in the TS type system.
At first I thought that adding type checking to JS was a neat idea, but every time I ran into a problem like this (i.e., the TS type system being too incomplete to accurately reflect what's going on in JS), I became more convinced that a dynamically typed language simply isn't meant to have static type checking. Either you create a language that is actually statically typed and you can do all the type checking you want (cf. C++), or you create a dynamically typed language, enjoy the added flexibility and accept that you can only do runtime checks. Not to mention the lack of accurate typings for third-party libraries. Type checking isn't going very well in Python, either.
I'm switching back to CoffeeScript, which has sane "class fields". This is an important reason for me to switch back, perhaps even the most important.
Reacted by Andrey Mikhaylov (lolmaus), Ticviking, JamesJMadden, Jonas and RAMPAGE
I don't know who came up with this first, but TS and ESnext basically share the same critical mistake to consider a "class field"
classA{prop='abc';}
equivalent to a constructor initializer
functionA(){this.prop='abc';}
instead of a default property on the prototype.
functionA(){}A.prototype.prop='abc';
I would argue that if you need a value to be the shared between every instance you should not allow yourself to use it with the same syntax that you'd access a member variable, because it may get confusing.
You can use the specifier static if you want to add a member to the constructor itself.
Besides the fact that this is inconsistent with member functions (which are set on the prototype), this causes all kinds of nasty problems. For example, you can't override a parent "class field" by setting a derived class prototype property.
Member functions are what prototypes are for, so that you can have a member invocation syntax on your function while implicitly passing the this argument. There is a clear difference in syntax in the way you declare a member and a property in Typescript/ES class, so a different behavior may be expected. If you want a function as a property you have the prop = function() { } syntax that you can use
If "class fields" were prototype properties instead, there wouldn't be any problem of this kind (below, I'm avoiding class field syntax in order to avoid confusion).
I think that the current issue ticket is a symptom of this problem being reflected in the TS type system.
At first I thought that adding type checking to JS was a neat idea, but every time I ran into a problem like this (i.e., the TS type system being too incomplete to accurately reflect what's going on in JS), I became more convinced that a dynamically typed language simply isn't meant to have static type checking. Either you create a language that is actually statically typed and you can do all the type checking you want (cf. C++), or you create a dynamically typed language, enjoy the added flexibility and accept that you can only do runtime checks. Not to mention the lack of accurate typings for third-party libraries. Type checking isn't going very well in Python, either.
This rant of yours is a bit unfair. TS type system is one of the most expressive (and complex) type system around, and can express most of the shenanigans that people have done so far with Javascript, because it has been designed with the intention to retroactively add typings to existing libraries. Even this particular case is an example of Typescript semantic actually expressing correctly what happens with ES classes (to be fair ES classes were inspired and modelled after Typescript classes so there is some blame in Typescript if they don't appeal to your liking).
As for "there is no use in do statical type check in a dynamic language" I'd argue that there is instead, and it's static analysis which can help reducing bugs: although Javascript is a dynamic language I'd argue that 99% of Javascript code should not use dynamic features and should be statically typable, in order to be "safer".
As for those awesome Javascript tricks that you can do since it is actually a dynamic language, they shouldn't cover more than 1% of all the Javascript code. And typescript provides you with escape hatches from the "safe" static types like the any type, so that you can write that 1% of magic dynamic code, add a warning for reviewers that they need to pay special attention to that part, then go back to the safety and happiness of a statically typed language.
I'm switching back to CoffeeScript, which has sane "class fields". This is an important reason for me to switch back, perhaps even the most important.
If adding non-function properties to a prototype is something that happens often in your ordinary workload it is a good idea to use a language that allows for that to be expressed easily. I have no experience with CoffeeScript, so I can't advise for or against it.
Hey pqnet, thank you for taking the time to respond to my post.
I would argue that if you need a value to be the shared between every instance you should not allow yourself to use it with the same syntax that you'd access a member variable, because it may get confusing. You can use the specifier static if you want to add a member to the constructor itself.
I might agree with this way of thinking for a static language like C++ or C#, but not for a dynamic language like JavaScript or Python. In dynamic languages, there is no danger in "confusion" because properties on the prototype (or "attributes" on the class in Python) are intended as a transparent fallback value. Alternatively, you could say that programmers are encouraged to "confuse" between instance and prototype properties; the language feature was introduced specifically to facilitate this.
The very idea is that you do not need to distinguish between an instance property and a prototype property. Reading the property on the instance, you do not need to be aware of the difference. Writing the property on the instance, you do not need to worry, because the property on the prototype will never be affected.
Member functions are what prototypes are for, so that you can have a member invocation syntax on your function while implicitly passing the this argument.
You write this as if meaning to imply that non-function members are not "what prototypes are for". I beg to differ; I think the creators of JavaScript added the option to define non-function prototype properties because they wanted them to be used. Indeed, they are almost universally used in JavaScript projects, and I think the use is valid in a large majority of cases.
It seems that the aim of the TS project is to instill an opinion on what language features of JS should and shouldn't be used. Is it an official standpoint that prototypes should have only function members? In any case, while I see the added value of type checking, I never asked for a language that changes the semantics of JavaScript.
There is a clear difference in syntax in the way you declare a member and a property in Typescript/ES class, so a different behavior may be expected.
If you mean that the assignment notation for class fields is consistent with its semantics, then I somewhat agree. In C++ (and I guess C# and Java as well), the same notation is used to set a default value for an instance property, too:
classExample {
int property = 0;
};
However, these languages have no equivalent of prototype properties, so there is no other plausible meaning for the notation, unlike in JavaScript.
In JavaScript, if class fields had the semantic of setting prototype properties, perhaps we would expect a colon notation instead, analogous to object literal notation:
classExample{property: 0;}
However, that notation is obviously unavailable for TypeScript, as it conflicts with type annotation syntax.
What really gripes me, is that a notation was invented for something that already had a convenient notation:
classExample{constructor(){this.property=0;}}
Thus leaving no obvious candidate for something that did not have a convenient notation yet:
classExample{}Example.prototype.property=0;
Contrast this with CoffeeScript, which has both colon notation and assignment notation inside class bodies, using both notations in consistent ways that add real value. Colon notation is used both for function and non-function prototype properties:
If you want to set an instance property in the constructor, you can, just like in JavaScript:
classExampleconstructor:->this.property=0
Assignment notation at class body scope is used for a purpose that does not exist in languages like C++, i.e., to store variables that can be used in logic that runs at class definition during runtime (in C++ you can run logic during class definition, too, but it happens during compile time and it requires different notation). Here is a slightly advanced example of how that may be used for metaprogramming purposes:
import { encoding } from'./config.coffee'classExampleutf8method=->### imagine lots of code ###utf16method=->### lots of different code ###method:if encoding is'utf8'then utf8method else utf16method
At first I thought that adding type checking to JS was a neat idea, but every time I ran into a problem like this (i.e., the TS type system being too incomplete to accurately reflect what's going on in JS), I became more convinced that a dynamically typed language simply isn't meant to have static type checking. Either you create a language that is actually statically typed and you can do all the type checking you want (cf. C++), or you create a dynamically typed language, enjoy the added flexibility and accept that you can only do runtime checks. Not to mention the lack of accurate typings for third-party libraries. Type checking isn't going very well in Python, either.
This rant of yours is a bit unfair.
I think I'm just stating a tradeoff. Surely you agree there is a tradeoff between static and dynamic typing? I just think that TypeScript is making a compromise, which ends up not working very well.
TS type system is one of the most expressive (and complex) type system around,
I will agree with that,
and can express most of the shenanigans that people have done so far with Javascript,
but I would like to nuance this. I think that 90% of normal JavaScript can be straightforwardly typed using TS annotations. Another 9% can be typed, but requires complex annotations or advanced techniques, which tend to be highly intimidating for beginners. The remaining 1% cannot be typed at all. Given the size of modern JavaScript projects, we end up hitting the undesirable 10% all the time.
because it has been designed with the intention to retroactively add typings to existing libraries.
This is how TS sometimes advertises itself, but it is not the whole story. From the way maintainers and proponents (including you) defend the ways in which TS departs from regular JS semantics, it is clear that another intention is to enforce a particular opinion about how JS should be used. Remarks like "that something is allowed does not mean you should do it" and your "prototypes are meant for member functions". The opinion of the TS compiler, and it proponents, appears to be that JavaScript code should be written like C# code, even though C# is static and class-based while JavaScript is dynamic and prototype-based.
Even this particular case is an example of Typescript semantic actually expressing correctly what happens with ES classes (to be fair ES classes were inspired and modelled after Typescript classes so there is some blame in Typescript if they don't appeal to your liking).
Exactly. I think it is madness that ES went along with it.
As for "there is no use in do statical type check in a dynamic language"
Note that this is not what I wrote. I think there is a use in principle, but it's simply not possible to do it in a way that is comprehensive, correct and convenient at the same time.
I'd argue that there is instead, and it's static analysis which can help reducing bugs: although Javascript is a dynamic language I'd argue that 99% of Javascript code should not use dynamic features and should be statically typable, in order to be "safer". As for those awesome Javascript tricks that you can do since it is actually a dynamic language, they shouldn't cover more than 1% of all the Javascript code.
Here you are making claims about what should and should not be done in JavaScript, confirming what I wrote above.
Dynamic typing makes the programming language more powerful. Expressive power makes programmers more productive. I'm in favour of consistency and I'm open to discussions about situations in which dynamic construct are better avoided, but I don't think a compiler for a dynamic programming language should be ruling out entire domains of dynamic semantics. The syntax of the language itself (i.e., ES) even less so!
And typescript provides you with escape hatches from the "safe" static types like the any type, so that you can write that 1% of magic dynamic code, add a warning for reviewers that they need to pay special attention to that part, then go back to the safety and happiness of a statically typed language.
I agree the escape hatch is there, but you need it a lot in order to avoid all the problems with TS. Even static types are often problematic.
If adding non-function properties to a prototype is something that happens often in your ordinary workload it is a good idea to use a language that allows for that to be expressed easily.
Interface implementation is public, mostly we would want protected members, take a look at following.
classBaseClass{protectedinit: (... args: any[])=>any;constructor(){// child classes can initialize members// before executing further statements...this.init?.(... arguments);}}classDerivedInstanceProperty{protectedinit=()=>{this.a="1";this.b="2";}}classDerivedMethod{// @ts-ignoreprotectedinit(){this.a="1";this.b="2";}}
This code ensures that init does not popup on intellisense.
Instance Member is 60% slower compared to Prototype Method. So argument of using instance member goes against performance.
Current workaround for using // @ts-ignore is an extra step, which might hide an important error by hiding wrongly typed parameteres or any other error.
classSuper{prop(){}}// this will remove `prop` from existence// howerver derived class can still override it// without typescript warningdeleteSuper.prototype.prop;classDerivedextendsSuper{prop(){}}
In case this is useful for anyone else, this work-around gives me error free "ts-checking" with vanilla javascript.
The core need is to allow a derived class to optionally define a function.
I use this pattern frequently as part of an opt-in event/callback system pushing the details of event registration/de-registration into the base class.
classMotorextendsEvents{onSunrise(a){turnOnMotor();}}classEvents{constructor(){if(this.onSunrise)addSunriseListener(this.onSunrise.bind(this));}dispose(){if(this.onSunrise)delSunriseListener();}}/** Define to receive a callback * @function * @param {Date} time time of sunrise */Events.prototype.onSunrise=undefined;
Activity
ajafff commentedon Oct 18, 2018
The problem here is that
prop(){}defined as method is placed on the prototype.prop: anyas property is written directly to the instance (if it had an initializer) and thus overrides the prototype method.Consider the following slightly modified version of your code:
You can still use a function to initialize
propinDerived:jgonggrijp commentedon Oct 18, 2018
Thanks Klaus Meinhardt (@ajafff), I didn't think of that. The reason I'm running into this is not resolved yet, though. A common pattern in the JavaScript world is to allow an object property to either be of type
Aor a bound method of type() => A. Underscore's and Lodash's_.resultis specifically meant to cater to this scenario. However, there seems to be no way in TypeScript to express such a typing:There may be a way I'm not aware of (I hope there is), but otherwise, I'd argue TypeScript is missing something.
RyanCavanaugh commentedon Oct 18, 2018
What is
Superdoing here? Do you mean to have an interface instead?jgonggrijp commentedon Oct 18, 2018
My specific situation is as follows:
But it also applies if the library were written in TypeScript in the first place.
RyanCavanaugh commentedon Oct 18, 2018
This does not mean "all syntactically-legal ES6 code must not have type errors"; see https://stackoverflow.com/questions/41750390/what-does-all-legal-javascript-is-legal-typescript-mean
This has come up in the past but the specific scenario of "this method may be overriden by a property of a different type" hasn't come up often enough to warrant a solution
jgonggrijp commentedon Oct 19, 2018
Fair point. I started this ticket with different assumptions, so the opening post and the title probably don't accurately reflect anymore what this issue is really about.
I'm not meaning to imply that TypeScript should just accept the example code I presented. Rather, the problem is that there seems to be no alternative. Semantically, it is a valid and even meaningful construction in JavaScript to override a base class method with a subclass non-function prototype property or vice versa. This is especially the case if the return type of the method is the same as the type of the non-function. To me, this seems like a scenario that could be type-checked, but again, there seems no way to do this in TypeScript.
If it hasn't come up often, then apparently not many Backbone users work with TypeScript. In Backbone,
_.resultis used all over the place. You may be able to gain some ground there!pchodak commentedon Jul 31, 2019
Got the same when using :
avonwyss commentedon Apr 24, 2020
For completeness (and people seeking help with TS2425 errors), you can always use type merging to "tell" TS that the member shall be seen as method.
The examples above for instance can be solved like this:
trusktr commentedon May 8, 2020
Unfortunately, that doesn't work well:
playground
This is a pattern I've seen in base classes before (in plain JS).
For example, here is a more representative example of what a base class may want to do (note the "ERROR 2425"):
Now strip all the type from that example, and you'll see that it is valid and well-functioning JavaScript.
trusktr commentedon May 8, 2020
Some people mention that we can just use an arrow function, like
but this is not ideal because
superin an arrow function to call the same function of a super class).Ryan Cavanaugh (@RyanCavanaugh) It seems that a super class having an initializer is the main foot gun that the current restriction aims to prevent.
Can we perhaps have a way to allow overriding properties with prototype methods under the condition that the super class must not have an initializer for the property being overridden (and with strict types therefore requires it to be an optional property)?
For example, what if
is valid with no type error, and
has a type error that says something like "The
Superbase class propertyprophas an initializer, therefore the subclassDerived2can not overridepropwith a prototype method."I think allowing this under certain conditions would be great!
githoniel commentedon Jun 28, 2020
I have an example here
Error message is
I know the difference between class prototype function and class property. We could fix the error by
But in as in #18654 says they should behave different in
strictmode.How can I do this if I want to use function properties typechecking here with extend class override here?
s-cork commentedon Feb 2, 2021
I came across this error when I wanted subclasses to be able to signal that the method does not exist by setting it to null.
What I want to be able to do
I can however change the definition for class A to:
But since this is valid typescript I would have assumed the first version was also valid...
I can see how it's not. I guess it's because prototype is itself an instance of
type Aand so assigningfooto be a function is like defining an instance member property. Typescript considers it different to defining an instance member function inside the class body, even though it all ends up on the prototype anyway.It would be nice if my original code worked, I don't mind the workaround but I don't love it.
Note the opposite is also problematic
(I also want to avoid defining functions that end up on the instance and would prefer to keep them on the prototype).
jgonggrijp commentedon Feb 2, 2021
I don't know who came up with this first, but TS and ESnext basically share the same critical mistake to consider a "class field"
equivalent to a constructor initializer
instead of a default property on the prototype.
Besides the fact that this is inconsistent with member functions (which are set on the prototype), this causes all kinds of nasty problems. For example, you can't override a parent "class field" by setting a derived class prototype property.
If "class fields" were prototype properties instead, there wouldn't be any problem of this kind (below, I'm avoiding class field syntax in order to avoid confusion).
I think that the current issue ticket is a symptom of this problem being reflected in the TS type system.
At first I thought that adding type checking to JS was a neat idea, but every time I ran into a problem like this (i.e., the TS type system being too incomplete to accurately reflect what's going on in JS), I became more convinced that a dynamically typed language simply isn't meant to have static type checking. Either you create a language that is actually statically typed and you can do all the type checking you want (cf. C++), or you create a dynamically typed language, enjoy the added flexibility and accept that you can only do runtime checks. Not to mention the lack of accurate typings for third-party libraries. Type checking isn't going very well in Python, either.
I'm switching back to CoffeeScript, which has sane "class fields". This is an important reason for me to switch back, perhaps even the most important.
[activerecord] Avoiding Promise type definition bugs (error TS2425)
[activerecord] Avoiding type definition bugs (error TS2425)
pqnet commentedon Mar 18, 2022
I would argue that if you need a value to be the shared between every instance you should not allow yourself to use it with the same syntax that you'd access a member variable, because it may get confusing.
You can use the specifier
staticif you want to add a member to the constructor itself.Member functions are what prototypes are for, so that you can have a member invocation syntax on your function while implicitly passing the
thisargument. There is a clear difference in syntax in the way you declare a member and a property in Typescript/ES class, so a different behavior may be expected. If you want a function as a property you have theprop = function() { }syntax that you can useThis rant of yours is a bit unfair. TS type system is one of the most expressive (and complex) type system around, and can express most of the shenanigans that people have done so far with Javascript, because it has been designed with the intention to retroactively add typings to existing libraries. Even this particular case is an example of Typescript semantic actually expressing correctly what happens with ES classes (to be fair ES classes were inspired and modelled after Typescript classes so there is some blame in Typescript if they don't appeal to your liking).
As for "there is no use in do statical type check in a dynamic language" I'd argue that there is instead, and it's static analysis which can help reducing bugs: although Javascript is a dynamic language I'd argue that 99% of Javascript code should not use dynamic features and should be statically typable, in order to be "safer".
As for those awesome Javascript tricks that you can do since it is actually a dynamic language, they shouldn't cover more than 1% of all the Javascript code. And typescript provides you with escape hatches from the "safe" static types like the
anytype, so that you can write that 1% of magic dynamic code, add a warning for reviewers that they need to pay special attention to that part, then go back to the safety and happiness of a statically typed language.If adding non-function properties to a prototype is something that happens often in your ordinary workload it is a good idea to use a language that allows for that to be expressed easily. I have no experience with CoffeeScript, so I can't advise for or against it.
jgonggrijp commentedon Mar 21, 2022
Hey pqnet, thank you for taking the time to respond to my post.
I might agree with this way of thinking for a static language like C++ or C#, but not for a dynamic language like JavaScript or Python. In dynamic languages, there is no danger in "confusion" because properties on the prototype (or "attributes" on the class in Python) are intended as a transparent fallback value. Alternatively, you could say that programmers are encouraged to "confuse" between instance and prototype properties; the language feature was introduced specifically to facilitate this.
The very idea is that you do not need to distinguish between an instance property and a prototype property. Reading the property on the instance, you do not need to be aware of the difference. Writing the property on the instance, you do not need to worry, because the property on the prototype will never be affected.
You write this as if meaning to imply that non-function members are not "what prototypes are for". I beg to differ; I think the creators of JavaScript added the option to define non-function prototype properties because they wanted them to be used. Indeed, they are almost universally used in JavaScript projects, and I think the use is valid in a large majority of cases.
It seems that the aim of the TS project is to instill an opinion on what language features of JS should and shouldn't be used. Is it an official standpoint that prototypes should have only function members? In any case, while I see the added value of type checking, I never asked for a language that changes the semantics of JavaScript.
If you mean that the assignment notation for class fields is consistent with its semantics, then I somewhat agree. In C++ (and I guess C# and Java as well), the same notation is used to set a default value for an instance property, too:
However, these languages have no equivalent of prototype properties, so there is no other plausible meaning for the notation, unlike in JavaScript.
In JavaScript, if class fields had the semantic of setting prototype properties, perhaps we would expect a colon notation instead, analogous to object literal notation:
However, that notation is obviously unavailable for TypeScript, as it conflicts with type annotation syntax.
What really gripes me, is that a notation was invented for something that already had a convenient notation:
Thus leaving no obvious candidate for something that did not have a convenient notation yet:
Contrast this with CoffeeScript, which has both colon notation and assignment notation inside class bodies, using both notations in consistent ways that add real value. Colon notation is used both for function and non-function prototype properties:
If you want to set an instance property in the constructor, you can, just like in JavaScript:
Assignment notation at class body scope is used for a purpose that does not exist in languages like C++, i.e., to store variables that can be used in logic that runs at class definition during runtime (in C++ you can run logic during class definition, too, but it happens during compile time and it requires different notation). Here is a slightly advanced example of how that may be used for metaprogramming purposes:
I think I'm just stating a tradeoff. Surely you agree there is a tradeoff between static and dynamic typing? I just think that TypeScript is making a compromise, which ends up not working very well.
I will agree with that,
but I would like to nuance this. I think that 90% of normal JavaScript can be straightforwardly typed using TS annotations. Another 9% can be typed, but requires complex annotations or advanced techniques, which tend to be highly intimidating for beginners. The remaining 1% cannot be typed at all. Given the size of modern JavaScript projects, we end up hitting the undesirable 10% all the time.
This is how TS sometimes advertises itself, but it is not the whole story. From the way maintainers and proponents (including you) defend the ways in which TS departs from regular JS semantics, it is clear that another intention is to enforce a particular opinion about how JS should be used. Remarks like "that something is allowed does not mean you should do it" and your "prototypes are meant for member functions". The opinion of the TS compiler, and it proponents, appears to be that JavaScript code should be written like C# code, even though C# is static and class-based while JavaScript is dynamic and prototype-based.
Exactly. I think it is madness that ES went along with it.
Note that this is not what I wrote. I think there is a use in principle, but it's simply not possible to do it in a way that is comprehensive, correct and convenient at the same time.
Here you are making claims about what should and should not be done in JavaScript, confirming what I wrote above.
Dynamic typing makes the programming language more powerful. Expressive power makes programmers more productive. I'm in favour of consistency and I'm open to discussions about situations in which dynamic construct are better avoided, but I don't think a compiler for a dynamic programming language should be ruling out entire domains of dynamic semantics. The syntax of the language itself (i.e., ES) even less so!
I agree the escape hatch is there, but you need it a lot in order to avoid all the problems with TS. Even static types are often problematic.
Right.
ackava commentedon Jul 21, 2022
Ryan Cavanaugh (@RyanCavanaugh)
Interface implementation is public, mostly we would want protected members, take a look at following.
This code ensures that
initdoes not popup on intellisense.Performance
https://jsfiddle.net/qkcatm81/
Instance Member is 60% slower compared to Prototype Method. So argument of using instance member goes against performance.
Current workaround for using
// @ts-ignoreis an extra step, which might hide an important error by hiding wrongly typed parameteres or any other error.RyanCavanaugh commentedon Jul 28, 2022
Akash Kava (@ackava) it seems like you're looking for
ackava commentedon Aug 16, 2022
Dirty Workaround
Most of the way of static code gen of methods
frostius commentedon May 3, 2024
In case this is useful for anyone else, this work-around gives me error free "ts-checking" with vanilla javascript.
The core need is to allow a derived class to optionally define a function.
I use this pattern frequently as part of an opt-in event/callback system pushing the details of event registration/de-registration into the base class.
Allow overriding Backbone model with a function
Allow overriding Backbone model with a function