-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Cannot do anything before super() #3311
Comments
There are similar restrictions in most OO languages so that the super-class constructor does not run with fields set to unexpected values. You can however do this:
Or provide appropriate getters in the parent class. |
How this related to non-
How this is different from non- Do you have valid point why it's not allowed? |
Linking to #1617 for bookkeeping purposes. |
You can call // This code does not have any errors
class Inherits extends Super {
test; // changed, was 'test = 1;'
view: Element;
constructor() {
let view = document.createElement('span');
super(view);
this.test = 1;
this.view = view;
};
} If you look at how class member initializers are emitted, the reasoning for this becomes fairly clear. Member initializers are emitted after the super call (otherwise your derived-class initializers wouldn't properly overwrite base class initializations); by enforcing the super call to be the first thing the compiler can guarantee that there's exactly one (correct) place to emit those initializers. You move the |
I know that, but it's not a reason to close this issue. It seems more like you was lazy to detect if Consider this code: class Test extends Something {
test:number;
test2:number;
test3:number;
test4:number;
test5:number;
test6:number;
test7:number;
test8:number;
test9:number;
test10:number;
test10:number;
test12:number;
test13:number;
test14:number;
test15:number;
test16:number;
test17:number;
test18:number;
test19:number;
test20:number;
test22:number;
test23:number;
test24:number;
test25:number;
test26:number;
test27:number;
test28:number;
test29:number;
test30:number;
constructor() {
this.test = 0;
this.test2 = 0;
this.test3 = 0;
this.test4 = 0;
this.test5 = 0;
this.test6 = 0;
this.test7 = 0;
this.test8 = 0;
this.test9 = 0;
this.test10 = 0;
this.test10 = 0;
this.test12 = 0;
this.test13 = 0;
this.test14 = 0;
this.test15 = 0;
this.test16 = 0;
this.test17 = 0;
this.test18 = 0;
this.test19 = 0;
this.test20 = 0;
this.test22 = 0;
this.test23 = 0;
this.test24 = 0;
this.test25 = 0;
this.test26 = 0;
this.test27 = 0;
this.test28 = 0;
this.test29 = 0;
this.test30 = 0;
super();
}
} Do you think this is okay code? Why then I need those member definitions before constructor at all? |
It's not just about class Base {
constructor(n) { }
}
var g = 10;
function f() {
g = 15;
}
class Derived1 extends Base {
i = f();
constructor() {
var x;
var y = (x = g, super(0), g);
console.log(x + ', ' + y)
}
} The code should (if we can make normative statements about things that aren't allowed) emit |
I suggest ignore such side effects and follow ES2015 spec here, because you cannot save developers from all of them and this "save" looks pretty strange. See this: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-super-keyword-runtime-semantics-evaluation and this: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-makesuperpropertyreference |
Trading correctness for convenience is not something we do lightly. I don't see the relevance of the linked test case -- there's no property initializer there; TypeScript's behavior is the same is 6to5's. Again, what do you propose the compiler emit for the example I gave? |
Wrong. You cannot do the same in TS, you need to do this, which is not the same (see #3311 (comment)): class Base {
_test: number;
constructor(v:number) {
this._test = v;
}
}
class Child extends Base {
test: number;
constructor() {
let test = 2;
super(test);
this.test = test;
}
} (es6 example was a bit wrong, here is correct one)
I am pretty sure it should do this. Again, I do not see difference between these two cases: [1] class Base {
constructor(v) {
this._test = v;
}
}
class Child extends Base {
constructor() {
var test = 1;
super(test);
this.test = getSomething();
}
}
function getSomething() {
return 1;
} [2] class Base {
constructor(v) {
this._test = v;
}
}
class Child extends Base {
test = getSomething();
constructor() {
var test = 1;
super(test);
}
}
function getSomething() {
return 1;
} But in TS you need to have duplicate definitions for first case (see #3311 (comment)) and second is not allowed in TS at all. |
Your example can be translated to an equivalent valid TS form (replace class Base {
constructor(n) { }
}
var g = 10;
function f() {
g = 15;
}
class Derived1 extends Base {
i = f();
constructor() {
super((Derived1.x = g, 0));
var y = g;
console.log(Derived1.x + ', ' + y);
}
private static x;
} The compiler emits for var Derived1 = (function (_super) {
__extends(Derived1, _super);
function Derived1() {
_super.call(this, (Derived1.x = g, 0));
this.i = f();
var y = g;
console.log(Derived1.x + ', ' + y);
}
return Derived1;
})(Base); Converting var Derived1 = (function (_super) {
__extends(Derived1, _super);
function Derived1() {
var x;
_super.call(this, (x = g, 0));
this.i = f();
var y = g;
console.log(x + ', ' + y);
}
return Derived1;
})(Base); So I think the answer is clear: emit property initialization section of derived class immediately after
Actually TS had failed to enforce "the super call to be the first thing", because developers can place almost arbitrary code in the parameter list of a super call, which executes before the super call. Java etc. also had failed to enforce it. So the "super call statement first" rule is not quite useful; disallowing access to |
The proposal is to remove the restriction on super being the first statement in the constructor body, and only require the existence of at least one call to super. |
@mhegazy and access to the |
this is kinda hard to do right all the time without control flow analysis.. this is why the restriction of class C {
x = 1;
constructor(v) {
if (v) {
super(); // tricks the compiler to think super was already called.
}
doSomething(this); // OK, but fails at runtime
super();
}
} |
Hmm.. seems reasonable. ES2015 will have such kind of errors reporting anyway, so people will get them at a runtime. Looks better than completely disallowing any statements before |
The argument of "users will get this error at runtime anyway" feels very weak -- it kind of defeats the purpose of static type checking. Compiler errors >> runtime errors. |
I think control flow analysis for int foo(int v) {
if (v) {
return 1;
}
} // Error: not all control paths return a value
int bar(int v) {
return 0;
printf("bar returned"); // Warning: unreachable code
} If it turns out too complicated for TS, how about just disallow |
@adidahiya Glad to know tslint, thanks! |
Approved. Access to |
should be addressed by #6860 |
Sorry if I'm misunderstanding something, but is this issue actually fixed (as opposed to "by design")? Should we be allowed to initialize subclass members outside the constructor or not, if the super call comes later in the constructor than the first line? The code in the OP comment gives me the same error today as was reported two years ago. |
The code in the OP is correctly an error. Because class member initializers are emitted immediately following the var x = 0;
class C extends B {
m = ++x;
constructor() {
x++;
super();
}
}
const c = new C(); It looks like |
@RyanCavanaugh Thanks for your answer. So, was anything "fixed" in this issue? Seems like it should be "declined" or "working as intended". Is the effect you describe actually unpredictable (i.e., it might change in future versions of TS) or just potentially confusing? If it is unambiguously true that member initializers are evaluated immediately after the I see what you mean about it "looking" like it would be var x = 0;
class C extends B {
m = ++x;
constructor() {
super(x++); // super constructor takes a parameter
}
}
const c = new C(); No error from TS, and The problem I see here is not with the location of the I know there's a permissiveness trade-off between allowing bad code and disallowing good code. There's good code that can be written by allowing both initializers and code before the super constructor. Do you have any more egregious examples of bad code that happens when you allow this? Thanks! |
It's more that the super call has to be in a position where we can simply and reliably insert assignment statements. For example, you could write class X extends Y {
k = this.foo();
constructor() {
const j = [expr1, super(), this.k] || super();
}
foo() { }
} which would be a legal way to write |
Ah, so are these backflips something you don't think TS should do in principle, or would they be something you'd consider a proposal/pull request for? |
It's more of a pragmatic thing - the amount of work we'd have to do to handle all the possible places in an expression a super call could appear and properly extract an equivalent program would be a substantial amount of complexity and testing, compared to just limiting users to choosing between property initializers or weird super call locations (which doesn't seem to be a big deal in practice). If there were some clever 20-line thing to pluck out an equivalent program for any arbitrary set of super calls, sure, but I expect this would be hundreds if not thousands of lines of code + testcases for the sake of a pretty small improvement. |
Right now TS emits
But, if we replace calls to
then we don't have to worry about trying to shove initializer statements in multiple or crazy places. The constructor body can be transcribed mostly as-is, and initializers are evaluated exactly once, immediately after the super constructor is called. This may or may not turn out to be a 20-liner does the right thing. Even if it is, I understand that you probably do not consider the issue important enough to tackle. In that case, could you please change the status of the issue to reflect what's actually going on (that this behavior is either intentional or not important enough to fix)? If you go back and read this issue from top to bottom, it's clear that @NekR and @duanyao are asking to always allow code before Whew, I wrote a lot. Thanks again for your time! |
Wow, I thought this was fixed. Why so you mark this fixed if it's not? You're saying about should be, predictable location, etc. But as I stated before, Babel handles this right and was handling it right when I opened this issue, TypeScript still not. I stopped using TypeScript for a while ago because of such small issue which nobody wants to fix because it's their beautiful software (yet advertised to use) and they have so principles of not changing things. It's a shame anyway and I'm sad for your tool's users. |
Evaluate class members first, then code before |
You have to initialize class members after
The issue that bothers @RyanCavanaugh is how to emit class member initialization immediately after the call to |
It was actually discussed already, just like babel does that. |
I mostly agree but the Babel REPL doesn't seem to know what do with |
Consider this code:
And it says:
2376 A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
I understand that it's because of
test
property. but can you allow not-this
code beforesuper()
call how ES6 does?Also, this won't work too, of course:
The text was updated successfully, but these errors were encountered: