Closed pdfernhout closed 2 years ago
Yeah, this an interesting issue because I personally have had to write special cases where you have to pass closures to the super to ensure they get executed before other blocks of code. There needs to be a way to allow for this.
On the other side of the coin, this is also an issue in C# as well because calling :base() you need to follow a similar pattern and have an onConstruct override or something like that.
Or... all your properties need to be lazy.. :/
I also have many cases where I check / initialise local variables before calling super. It's great that TypeScript uses the best from other well-typed languages, but the way it is right now is simply out of line with common sense. Like per @jbaron example:
constructor(id:String) {
var label = I18N.translate(id);
var icon = IconMap.get(id);
super(label, icon);
this.setBackground(this.color);
}
// vs…
constructor(id:String) {
super(I18N.translate(id), IconMap.get(id));
this.setBackground(this.color);
}
That limitation doesn't bring in any value. There was an argument on complexity of the checks – doing check for this
use before super
shouldn't be hard, this is also the same logic used in Swift, which inherits the best from C languages.
It pretty much looks like this is fixed for 2.0 beta. Can you give it a try @pdfernhout?
@DanielRosenwasser It's probably all right now. The error message explains what went wrong precisely I think.
This is fine:
class MyClass {
constructor(public str: string) { }
}
class OtherClass extends MyClass {
constructor(str: string) {
var that = str;
super(str);
}
}
This is not:
class MyClass {
constructor(public str: string) { }
}
class OtherClass extends MyClass {
constructor(public str: string) {
var that = str;
super(str);
}
}
Could someone tell me why this code is not permitted ?
`export class MatterAccessRevokedForUser extends MatterUserDomainEvent {
constructor();
constructor(tenant: string, fileNumber: string, username: string);
constructor(tenant?: string, fileNumber?: string, username?: string) {
if (arguments.length === 0) {
super(null, null, null);
this.constructor_MatterAccessRevokedForUser_0();
return;
}
super(tenant, fileNumber, username); //ERROR: super must be called before this
this.constructor_MatterAccessRevokedForUser_1(tenant, fileNumber, username);
}
private constructor_MatterAccessRevokedForUser_0(): void {
}
private constructor_MatterAccessRevokedForUser_1(tenant: string, fileNumber: string, username: string): void {
}
}`
This example clearly shows the need to allow code before super call when it does not use "this" to apply transformations to the parameters of super()
Error:
constructor(opts: ReadableOptions) {
opts.objectMode = true
super(opts)
}
My solution
constructor(opts: ReadableOptions) {
super((() => {
opts.objectMode = true
return opts
})())
}
Is there any reason why having variable initiators on private variables in the sub class disables the above functionality?
private _something = false;
constructor(args:any) {
Guard.EnsureNotNull(args);
super(args.somethingElse);
}
Moving the variable initialisation so it's after the super call fixes it - but I'm curious as to the reasoning as by definition private variables should not have any side effects on the base class?
@davidglezz this is exactly what I've had to do.
@DanielRosenwasser I confirmed things works better under TypeScript 2.6. I did not test earlier 2.x versions. The Handbook page on Classes also reflects this improvement. Thanks to everyone who worked on those improvements. There are still edge cases that could be improved further like @marlon-tucker and @captainjono raised -- but as far as I am concerned the fix so far is good enough.
=== More details
Below is the test example I used -- where updatedStr
can now be calculated before the super()
call because of this improvement. Three comments in the example show what is still not permitted.
The third commented code statement contains a direct access to this
before super()
and is not permitted at any time. I doubt that many people would find that limit problematical -- and it also reflects a specific limit in ES6.
The first two commented code items are for an initialized property and a parameter property. They are both an indirect access to this
which is not allowed only because there is code for the calculation before super()
. As with @marlon-tucker's question, I feel one could still quibble about those two indirect this
constraints since those initializations using this
are something that could presumably be done after the super()
call in any code generated by TypeScript. However, it is straightforward to work around those two limits by avoiding those indirect this
-accessing constructs and doing the initialization yourself manually in the constructor after the super()
call.
An edge case involving conditional code with a presumably analyzable bifurcation in the code path to call super()
in two different but exclusive ways was pointed out by @captainjono (and is not included in the example). Workarounds include using temporary variables and flags to surround just one super()
call or to instead do some application redesign perhaps with a factory class. While the edge case pointed out there seems like it should be supported in theory, in practice I wonder if that edge case is very common -- as it just feels to me like having more than one super()
call in a constructor is asking for trouble.
Further improvements in those directions might be nice -- but such work is at risk of producing diminishing returns after the previous improvement. So, it is OK with me to close this issue and suggest people open a new issue if they want to continue discussing relaxing constraints further in such cases.
class MyClass {
value: string;
constructor(str: string) {
this.value = str;
}
}
class OtherClass extends MyClass {
// Not permitted indirect this access via initialized property: x: string = "Baz";
constructor(/* Not permitted indirect this access via parameter property: public y: 10, */ str: string) {
const updatedStr = str + "FooBar"; // <-- no longer has an error thanks to fix
// Not permitted direct this access via statement: console.log("test" + this.value);
super(updatedStr);
}
}
@mhegazy hi what kind of pr that this issue accept😅
The implementation details in #29374 are starting to reach into the transformer realm, so a summary of the findings (thanks @ajafff for pointing much of these out!):
this
can be referenced, such as:
const x = class extends this.someProp { }
const x = { [this.someProp]: true };
super(...)
call, so its logic makes assumptions based on that. See https://github.com/Microsoft/TypeScript/pull/29374#discussion_r247328358.The second point means this now requires some more changing to the checker and transformer. I think it's still doable if the original "you must call super immediately" requirement becomes "you must call a super before the end of the function or references to this or super" using control flow analysis. In other words, >=1 super(...)
call must be reached by the end of the constructor.
class Test extends Base {
prop = 1;
// Not acceptable, as the `super(...)` might not be reached
constructor() {
if (Math.random() > .5) super(1);
}
}
class Test extends Base {
prop = 1;
// Acceptable, as there's no way for a `super(...)` not to be reached
constructor() {
if (Math.random() > .5) { super(1); }
else { super(0); }
}
}
On top of that, super
and this
cannot be referenced until the line where TypeScript would add parameter properties or property initializers. See https://github.com/Microsoft/TypeScript/pull/29374#discussion_r
Related but not the same: #23422 on the subject of multiple super()
s.
Because of this limitation, we can write
let instance: Foo | null = null
class Foo {
static singleton() {
return new Foo()
}
private constructor() {
if (instance) return instance
instance = this
}
}
but we can write this:
let instance: Foo | null = null
class Foo extends SomeOtherClass {
static singleton() {
return new Foo()
}
private constructor() {
if (instance) return instance
super()
instance = this
}
}
We can change constructor
to the following, but it causes performance issues:
private constructor() {
super() // unnecessary call.
if (instance) return instance
instance = this
}
Why not at the very least allow expressions-with-no-this before super()
if the super()
expression is on its own line, to make implementation easier to start with? And throw an error otherwise?
That'd probably allow the vast majority of use cases to work.
This just bit me in the foot. I ran into a case where the super()
being before if (instance) return instance
caused a huge performance bottleneck with regards to how consuming code was using a singleton.
It'd be great to allow at least simple expressions before super()
if super()
is on a single line. 👍
There seems to be inconsistency between how TypeScript handles properties.
Consider this code:
// TypeScript
class Foo {}
class Bar extends Foo {
prop = 1
constructor() {
console.log("foo") // <-- A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.(2376)
super()
}
}
TypeScript transforms the code to this:
// TypeScript transformed JS
class Foo {}
class Bar extends Foo {
constructor() {
this.prop = 1; // <-- TypeScript puts the initializer before super, hence the error
console.log("foo");
super();
}
}
While Babel does this:
// Babel transformed JS
class Foo {}
class Bar extends Foo {
constructor() {
console.log("foo");
super();
_defineProperty(this, "prop", []); // <-- Babel puts the initializer after super
}
}
Oddly enough if you take out the code before super, TypeScript then decides to do the initialization after super:
// TypeScript
class Foo {}
class Bar extends Foo {
prop = 1
constructor() {
super()
}
}
Gets transformed to:
// TypeScript transformed JS
class Foo {}
class Bar extends Foo {
constructor() {
super();
this.prop = 1;
}
}
Babel transforms the code the same way no matter if code is put before super or not. It allows code to be placed before super as long as it doesn't access this
. Many other languages allow localized code as long as the instance isn't access (since it isn't initialized).
It appears that TypeScript also allows this, as long as you don't initialize the property inline. If you do the initialization manually inside the constructor it works as expected.
IMO, this is a bug. Property initialization should be done the same way regardless if code is placed before super or not. Babel does this correctly while TypeScript currently does not.
I agree with this! Recently I have come upon this issue multiple times in my code base:
abstract class A {
protected constructor() {
this.init();
}
protected abstract init(): void;
}
class B extends A {
private readonly methodBound = this.method.bind(this);
constructor() {
super();
}
protected init(): void {
console.log(this.methodBound); // undefined
console.log(this.method); // reference to method
}
private method(): void {
// do something
}
}
Being able to do this instead would be so nice:
class B extends A {
private readonly methodBound: () => void;
constructor() {
this.methodBound = this.method.bind(this);
super();
}
protected init(): void {
console.log(this.methodBound); // works!
console.log(this.method); // reference to method
}
private method(): void {
// do something
}
}
@piotrgajow Your example code uses this
before the super call. The property also initializes after super is called. That will never change.
The issue we're discussing is allowing code before super() that does not touch this
. Generally it involves code that mutates arguments that get passed into super.
Usually you can put code before super() that does not touch this
. But for some reason the TypeScript compiler disallows all code when you have a property w/ a default value declared.
I've run into this as well, simply messing with parameters before passing them to super()
. I'm using babel to compile it, and babel outputs correct, functional code, so I'm just // @ts-ignore
ing the constructor. 😞
@lukescott Yeah I know, maybe I did not express myself clearly. What I meant was that such strict TypeScript compiler behaviour does not protect the developers from making mistakes anyway (as in the example I provided) but it limits them in certain ways, resulting in workaround and less readable code. Thus I also think that code before super call should be allowed.
Preventing "statements that does not touch this" before "super statement", leads to ugly code at first, but as well prevents construction objects in valid state.
In below example, in line with comment "Complex calculation here", we do calculate valid value for super parameter (this calculation might take a few lines/statements). As of now, we would have to either:
All cases in this playground
class A {
#a: number;
constructor(a: number) { this.#a = a; }
}
class B extends A {
#c: number;
constructor(c: number) {
const x = c + 1; // Complex calculation here
super(x);
this.#c = c;
}
}
In short: Is this blocker? Nope! Tones of hacks to be used. Does current limitation help TS maintainers? Probably yes as it simplifies a lot for them. Is current behavior natural for newcomers to TS? I doubt so, they have to simple learn to avoid this odd thing.
This limitation seems to not bring safety to written code, it just brings limitations.
I think I found a bug, but since it’s directly related to this I’m not sure if it needs a separate issue.
In the comments above (https://github.com/microsoft/TypeScript/issues/8277#issuecomment-214467386, https://github.com/microsoft/TypeScript/issues/8277#issuecomment-333302344), people are talking about passing anon functions / immediately-invoked-function-expressions (IIFEs) as arguments to super()
to circumvent the “no code before super” rule.
constructor(opts: ReadableOptions) { super((() => { opts.objectMode = true return opts })()) }
The bug I’ve found is that this IIFE can reference this
, which (since it’s in an arrow function) refers to the current class.
constructor (opts: ReadableOptions) {
super((() => {
console.log(this) // oh no!
opts.objectMode = true
return opts
})())
}
TypeScript in its current version (v3.9.2) does not emit an error, but when attempting to run this code, the JavaScript runtime will throw this error:
ReferenceError: must call super constructor before using 'this' in derived class constructor
You can see a full playground here.
Since Design Goal 1 is “Statically identify constructs that are likely to be errors”, I would propose adding to this issue by emitting an error for this use case.
TLDR: An IIFE passed as an argument to super()
should not be able to reference this
.
I just encountered error ts(2376). As far as I could see my use case hasn't been mentioned in this thread, so I'll add it as another motivation why it's desirable to allow other code before super()
.
In my case, the super constructor has side effects that must only run if the object instantiation is successful; and the derived class must validate its constructor arguments and throw on invalid values. It looks like this:
abstract class Node {
#parent: Node | null;
#children = new Set<Node>();
constructor(parent: Node | null) {
this.#parent = parent;
if (parent) parent.#children.add(this);
}
destroy() {
this.#parent?.children.delete(this);
}
}
class TextNode extends Node {
#text: string;
constructor(parent: Node, text: string) {
if (!text) throw new Error("`text` is required");
super(parent);
this.#text = text;
}
}
Moving the line if (!text) ...
after the super call introduces a memory leak because parent.#children
will still have a reference to the created object. But the TextNode constructor throws, so the caller can't obtain a reference to the object to call destroy()
on it.
I think I found another bug: Calling super()
within a try {}
block but not in the catch {}
block does not emit an error.
class Foo {
constructor (n: number) {
if (n < 0) throw new RangeError()
}
}
class Bar extends Foo {
constructor (n: number) {
try {
super(n)
} catch {
// Expected a TS error here:
// > Constructors for derived classes must contain a 'super' call.
}
}
}
new Bar(-1) // runtime error (not predicted by TS):
// > must call super constructor before using 'this' in derived class constructor
// > Must call super constructor in derived class before accessing 'this' or returning from derived constructor
I would like to add to this an argument why allowing code before the super call might be a good idea. My argument is the following: not allowing to call super after regular code will entice young programmers to call super with dummy values just to get it initialized. This will lead to existing objects that are in an dangerous internal state. Add lazy loading to the mix and you have essentially a race-condition generator.
In other words, if there can be no regular code before calling super, people are enticed to do that after super which will leave the object in a dangerous state for a short time. You don't see that since every member is initialized!
Here is a very long example (please only read that if you have time for an coffee break):
This is loosely based on a bug I just found in our code-base. Please note, we started with pure JavaScript and have converted this code to TypeScript. In JavaScript you can call the super later, therefore this code is written this way (I assume, I didn't write it).
class FancyThing extends Thing{
constructor(){
//now we do a lot of config stuff, I wrote so much dummy code to show how big the code is
let fancyConfigBuilder = new configBuilder(fancy);
let configA = fancyConfigBuilder.getConfig(also_fancy); //several lines of fancy configuration code as a javascript object
let configB = fancyConfigBuilder.getConfig2(also_fancy)//some user specific stuff
let configC = { //several lines of fancy configuration code };
super(configA, configB, configC);
}
}
The problem is that you can't write this since you needs to call super first. So what we could have done:
class FancyThing extends Thing{
constructor(){
super(new configBuilder(fancy).fancyConfigBuilder.getConfig(also_fancy), new configBuilder(fancy).fancyConfigBuilder.getConfig2(also_fancy), { //several lines of fancy configuration code as a javascript object, writen in human readable form }); //boy this is ugly and there is not a monitor in the world to show the whole code
}
}
There are two problems her: the code is way too long (this is our fault since we made this mess) and we can not use the same configBuilder object! The problem is, if that object has an internal state you basically have to handle this object via an parameter even if that doesn't make sense.
Alright, someone came up with a easy but pretty stupid solution:
class FancyThing extends Thing{
constructor(){
super(dummy, dummy, dummy); //really bad idea
initFancyThing();
}
initFancyThing(){
let fancyConfigBuilder = new configBuilder(fancy);
let configA = fancyConfigBuilder.getConfig(also_fancy); //several lines of fancy configuration code as a javascript object
let configB = fancyConfigBuilder.getConfig2(also_fancy)//some user specific stuff
let configC = { //several lines of fancy configuration code };
this.configA = configA;
this.configB = configB;
this.configC = configC;
}
}
And it worked. Until someone did basically this:
class FancyThing extends Thing{
constructor(){
super(dummy, dummy, dummy); //really bad idea
initFancyThing(); //will now load lazily, but we don't use a better dummy
}
async initFancyThing(){ //now with lazy loading
//can take 1 ms to 9 hours depending on the weather
}
}
And this was the point where all objects figuratively exploded if the internet connection was too fast or your device was too fast or the sun was felling that way. And since you work in a nice office with good internet connection this bug isn't too obvious.
TLDR; If we can't run regular code before super it will entice the usage of dummy values for the super call. This can lead to really ugly bugs.
Here's a toy example:
declare class T {
constructor(x: number, y: number);
}
class H extends T {
x = 0;
constructor() {
const rand = Math.random();
super(rand, rand);
}
}
this cannot be represented in TS.
Actually, it can, like this:
declare class T {
constructor(x: number, y: number);
}
class H extends T {
x = 0;
static rand = NaN;
constructor() {
super(H.rand = Math.random(), H.rand);
}
}
that is ugly, but TS allows it. ¯\(ツ)\/¯
Otherwise, I strongly believe that TS2376 is obsolete with the introduction of TS17009.
Since Design Goal 1 is “Statically identify constructs that are likely to be errors”, I would propose adding to this issue by emitting an error for this use case. An IIFE passed as an argument to
super()
should not be able to referencethis
.
TypeScript should not do this, an arrow function can be passed to super(() => ...)
and stored on the instance for use after the constructor
has finished.
As an example, this is a simplified version of something I'm already doing today (and works):
class MathView {
readonly #render: () => Element;
constructor(render: () => Element | Promise<Element>) {
this.#render = render;
}
async render(): Promise<Element> {
const element = await this.#render();
return wrapAndSetMetadata(element, this);
}
}
class MathPlusView extends MathView {
readonly addend: View;
readonly summand: View;
constructor(addend: View, summand: View) {
super(() => this.#render()); // This works fine, when the public
// .render() is called, "this" will be available
this.addend = addend;
this.summand = summand;
}
#render = async () => {
const [addendElement, summandElement] = await Promise.all([
this.addend.render(),
this.summand.render(),
]);
return fromTemplate`
<mrow>${ base }<mo>+</mo>${ superScript }</mrow>
`;
}
}
Now TypeScript could detect "this" within IIFEs within super()
, but once we can include code before super()
there wouldn't be any real reason to use such a hack.
@Jamesernator An IIFE (immediately-invoked function expression) is a function that is called immediately after it is defined. In your example, you have a regular arrow function, but it’s not called right away — as you correctly state, it doesn’t get called until the public .render()
is called, and by that time the this
(from inside the arrow function) has already been constructed. That’s why your code works.
In my example, an IIFE is sent as an argument to super
, and this fails at runtime because this
is referenced before it is constructed.
super( (() => {
console.log(this) // ReferenceError!
})() )
// ^ notice the call here — the function expression is invoked immediately
Your code poses no danger, as the argument sent to super
is in fact a function that doesn’t get called until later. My code sends a value (which just so happens to be a function call) that involves the use of this
before it gets constructed, and that’s what makes it dangerous. So TypeScript should do something to differentiate between the two scenarios. A naïve “Hey, there’s a this
in this here function!” is not sufficient.
ECMAScript class fields have arrived at Stage 4.
In the standard, we can write expressions that don't contain this
and super
before super()
. As people mentioned in this thread, that is very useful. I hope TypeScript allows such a code.
Not using Typescript syntax cuz i hate compiling, but i use checkJS this fails:
class A {}
class B extends A {
#x = ''
constructor (x, y, z) {
if (arguments.length < 3) throw new TypeError('Need more arguments')
super()
}
}
A 'super' call must be the first statement in the constructor when a class contains initialized properties, parameter properties, or private identifiers.ts(2376)
Typescript is only a compilation tool ..I expect to be able to make assertions during runtime, as a protection against unexpected inputs! Here is my take on the problem
class AssertionError extends Error {}
function assert<Value=unknown>(value: Value, assertion: string): asserts value {
if (!value) {
throw new AssertionError(assertion);
}
}
function assertNonNull<Value=unknown>(value: Value, valueName?: string, extra?: unknown): asserts value is NonNullable<Value> {
assert(value !== undefined && value !== null, 'Expecting a defined and non-null value');
}
class Base<Value extends unknown> {
protected _value: Value;
constructor(value: Value) {
this._value = value;
}
}
// IMPLEMENTATION 1 : desired usage
class Implementation<Value extends string> extends Base<Value> {
constructor(value: Value) {
assertNonNull(value); // typescript compiler-god is striking with great lighting
super(value);
//
// instance-related initialization...
//
}
}
const instance1 = new Implementation(null as any);
// IMPLEMENTATION 2 : workaround with factory pattern
class Implementation<Value extends string> extends Base<Value> {
private _safelyConstructed: boolean = false;
// generic type can't be passed to static, but could be in a `type` declaration above if DRY is required
static construct<Value extends string>(value: Value): Implementation<Value> {
assertNonNull(value);
const instance = new Implementation<Value>(value);
instance._safelyConstructed = true;
return instance;
}
constructor(value: Value) {
super(); // typescript compiler-god is appeased and laughing at the sweating developer
assert(this._safelyConstructed, 'Expecting instance to be safely constructed');
//
// instance-related initialization...
//
}
}
const instance2 = Implementation.construct(null as any);
// const instance2 = Implementation.new(null as any); // alternative name, it is valid
// const instance2 = new Implementation(null as any); // this is now unsafe...
And yes, assertion could be done in the base class in some cases, but not if some inputs are narrowing the base class types...
This pattern can be used for all the above cases too, essentially the constructor is wrapped with before and after logic. Although the construction function is static
, it can access everything in the class instance, even the private members.
Also, all classes extending an implementation with the factory-style workaround (actually the whole implementation tree extending it) will have to use the same pattern. So this may be cumbersome on the long run.
Here is another approach using a hook pattern. This is one is more complex, but allows the syntax and interfaces to stay the same.
class AssertionError extends Error {}
function assert<Value=unknown>(value: Value, assertion: string): asserts value {
if (!value) {
throw new AssertionError(assertion);
}
}
function assertNonNull<Value=unknown>(value: Value, valueName?: string, extra?: unknown): asserts value is NonNullable<Value> {
assert(value !== undefined && value !== null, 'Expecting a defined and non-null value');
}
// IMPLEMENTATION 1 : desired usage
class Base1<Value extends unknown> {
protected _value: Value;
constructor(value: Value) {
this._value = value;
}
}
class Implementation1<Value extends string> extends Base1<Value> {
constructor(value: Value) {
assertNonNull(value); // typescript compiler-god is striking with great lighting
super(value);
//
// instance-related initialization...
//
}
}
const instance1 = new Implementation1(null as any);
// IMPLEMENTATION 2 : workaround with internal hook pattern
class Base2<Value extends unknown> {
protected _value: Value;
constructor(value: Value) {
this._value = value; // assignment to appease the typescript compiler-god
this._construct(value);
this._init();
this._start();
}
// these abstract could be a base implementation instead, that can be overriden
abstract protected _construct(value: Value): void; // could use `this._value` since assignment was required by type
abstract protected _init(): void;
private _start() {
// base implementation
};
}
class Implementation2<Value extends string> extends Base2<Value> {
constructor(value: Value) {
super(value); // typescript compiler-god is appeased and laughing at the sweating developer
//
// instance-related initialization AFTER all the construtor hooks (construct, init, start) ...
//
}
protected _construct(value: Value) {
assertNonNull(value);
}
protected _init() {
// specific logic for this construction step, required since it is `abstract` in base class
};
// lets suppose `start()` is fine as it is already base-implemented
}
const instance2 = new Implementation2(null as any); // yay, this is safe and with the expected syntax
Unlike my previous attempt, the instanciation logic and interface stays inside the class encapsulation. It also allows the base class to expose a specific sequence/strategy of construction, which may help readability and decoupling. Essentially, use constructor
for assignments only and extract everything else.
Unfortunately, if omitting assignments in constructor
, typescript control flow may kick in tell that a value is not assigned if null
and undefined
is not possible for the type. And sadly, my assertion assertNonNull
does not have any effect... The only way I can think of is dirty but works:
this._value = undefined as Value;
If this is used, the _construct()
method will have responsibility of doing the assignment, but if forgotten Typescript will be blind to the omission. The other way around is allowing type Value | undefined
, but the control flow will warn everywhere that the type can be undefined
.
If async
or complex operations are required during construction, this method solves it as the contructor can't return a Promise
type.
Like my previous workaround, all the derived sub-classes would need to call the construction implementation when overriding an existing hook, which moves responsibility of sub-classes to know what to do (generally calling super
above is safer):
_construct(value: Value) {
// logic before, requires base class knowledge
super._construct(value); // in theory optional, but only if the overriden logic IS optional, requires base class knowledge
// logic after
}
@isometriq I believe that your suggestion about TSC emitting runtime assertions is off-topic here, maybe you'd like to make an issue about it? That idea is also against TS's goals though; just a fair warning.
@CrimsonCodes0 actually, Im the one emitting assertions using my functions. My workarounds are to be able to use them in a implementation class before the super()
call, like the others in this thread who have their reasons. Maybe my situation with assertions is misguiding, but my main concern here was about the workaround for the super()
enforcement rule of TSC.
You are right indeed, perhaps I should also open another ticket about the super call in combination with function with a asserts
return type, since I expect the compiler and control flow to understand that.
In my app, I'm trying to validate critical input with assertions (again not automatic compiler directives) to make sure my algorithms run well. I think you mean that I expect TSC to throw error automatically. After clarifying, would you say it is again TS goals? if yes, please explain.
[edit] I just read now about the subject and I see that what I call type-casting is also refered as "type assertion" (versus type inference). By type-casting I mean this:
const a = b as number;
So to be clear, when I mention assertion, I mean to check a critical condition that should throw or be handled if condition is not met.
The TypeScript specification currently reads:
The first statement in the body of a constructor must be a super call if both of the following are true:
- The containing class is a derived class.
- The constructor declares parameter properties or the containing class declares instance member variables with initializers.
Probably it's very easy that we add the condition "the target version is older than ES2022." Because ES class fields don't have such a limitation, we can safely remove the compile error without extra cost if the compilation uses the native feature.
@CrimsonCodes0 actually, Im the one emitting assertions using my functions. My workarounds are to be able to use them in a implementation class before the
super()
call, like the others in this thread who have their reasons. Maybe my situation with assertions is misguiding, but my main concern here was about the workaround for thesuper()
enforcement rule of TSC.You are right indeed, perhaps I should also open another ticket about the super call in combination with function with a
asserts
return type, since I expect the compiler and control flow to understand that.In my app, I'm trying to validate critical input with assertions (again not automatic compiler directives) to make sure my algorithms run well. I think you mean that I expect TSC to throw error automatically. After clarifying, would you say it is again TS goals? if yes, please explain.
[edit] I just read now about the subject and I see that what I call type-casting is also refered as "type assertion" (versus type inference). By type-casting I mean this:
const a = b as number;
So to be clear, when I mention assertion, I mean to check a critical condition that should throw or be handled if condition is not met.
Ah, my apologies, I had severely misunderstood what you were asking for. Yes, the problem that we all share in common here is the idea of having any code executing before super(...)
, even an assertion.
Honestly, I don’t see the point in forcing people to have a wrapper function around constructors, or to have long hardly readable expressions as parameter to super()
because TSC disallows even declaring constant to split these expressions in parts. I neither see the point in disallowing checking the parameters received by the derived class’s constructor unless it is only after the call to super()
, while failing before would make more sense.
Do the ECMAScript standard really disallows it? It seems not …
Is there any chance this error can be disabled for ES5? To my knowledge, this only applies to ES6 classes. The following example works in ES5 but not in ES6:
class Base {
name: string;
}
class Derived extends Base {
constructor() {
this.name = "Derived";
super();
}
}
My project has a fair amount of legacy code using ES5 standards and we see many of these errors from the compiler, even though the compiled code works as expected. It would be a significant and error-prone lift to fix all the issues. Our legacy code uses Backbone.js which requires some properties to be set before a super()
call. There are workarounds that could be used but, as I mentioned, our code works as is when targeting ES5 and we have no intent to upgrade to ES6.
We don't turn off semantic rules based on the target, since that creates a giant upgrade trap without people realizing it (as you yourself are noticing).
I'm curious: How did you get into this state in the first place? This has been an error in TS approximately (literally?) forever.
Appreciate the response and I understand the dilemma. Here's ours:
My team adopted TypeScript fairly early and we upgraded as much as we could until we started seeing this error. We're currently stuck on 1.6. We have a fair amount of legacy code still in use and we lack the resources to make the upgrades necessary to be compliant with newer TypScript versions. We're using Visual Studio as our IDE, which stopped supporting TS 1.x a few major versions ago.
The developer experience in our legacy code is about as bad as it gets: broken Intellisense and our MsBuild targets fail to find the resources to even output error messages. We get the same error in the post below but the recommended solution does not work for us.
https://forum.ncrunch.net/Default.aspx?g=posts&m=5355
However, our TS code still compiles valid JS with configuration settings of noEmitOnError: false
and target: ES5
but it's a soul-crushing process without any of the TypeScript tools.
Our legacy code is written in Backbone/Marionette, which uses ES5 style classes so the "this before super()" isn't a problem, though I wouldn't say it's encouraged. There's a long thread about it below. We've been using this
before super()
for so long that the other recommended solutions in the post would be a large, error-prone lift.
https://github.com/jashkenas/backbone/issues/3560
In the end, I just want to make our development process less painful. We don't really need to upgrade our TS version but it seemed like the path of least resistance so long as we could figure out how to mute this specific error.
My initial solution was a script to place //@ts-ignore
before every offending line. It works but creates a lot of //@ts-ignore
pollution and also prevents the compiler from reporting any other errors that may be found on that line.
Currently, I'm trying to make use of loose-ts-check to mute this specific error. I need to tinker with it a bit more to properly integrate it into our build and development process, but I believe it will provide a better path forward.
If you have any suggestions on how to move forward, I gratefully welcome them.
I'm not super familiar with Backbone's API - but if you're willing to, I would create an intermediate non-ES6 class that performs the constructor initialization in a deferred way.
So super()
really does nothing, and then you can run the "true" super through that non-ES6 class' helper method.
Here's a proof-of-concept in the playground.
This wouldn't work in ES2015 output since you'd need to use Reflect.construct
, but you could add it pretty easily.
Anyway, for any classes that need to be refactored, you can use that trick and write a lint rule to enforce that the helper method is actually called. For any classes where you need to access prototype methods before the super helper, you'd need to do some manual tweaking.
~@DanielRosenwasser this seems kind of silly. Code that doesn't access this
is valid prior to the super call. TS is in the wrong here.~
~https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super~
Correction: TS was in the wrong. More recently it was me
@dcporter I think you may have misunderstood. The issue is that I do need/want to access this
before super
, which is valid when using ES5 but not ES6. I believe the TypeScript team has addressed the issue of not allowing "this-less" code before super
.
@DanielRosenwasser Thank you for your response and code sample.
You're right, I thought your issue was the same one. Cheers, glad you're all set!
Has TS changed its policy towards pre-super code in general? Hoping to be wrong twice in one post 😄🤞🏻
I'd have to read through this thread and the changelog to get a precise idea of the current status, but, yes, you can execute "this-less" code in the constructor
before super
.
Fantastic news that I should have gotten previously and on my own. Thanks! Updated my OP.
The TypeScript specification currently reads:
It is reasonable in TypeScript to not permit
this
to be referenced in a constructor before callingsuper
when there are initialized properties or constructor parameter properties becausethis
is not fully initialized until aftersuper
is called. But broader restrictions on calling other code beforesuper
that is not directly usingthis
don't seem that helpful and can be worked around anyway. So why keep them?A common use case for having code before a call to
super
is to transform constructor parameters in the subclass constructor before passing them to the superclass constructor. If such transformations are complex, a programmer might want to do the transformation step-by-step on multiple lines for increased readability and easier debugging.An example of bypassing the compiler's restriction of no code before
super
is just making function calls wrapping arguments to asuper
call such assuper(logThisName(name))
where the called function refers tothis
.As show by an example in the Handbook discussion linked below on improving the explanation for TypeScript constructor restrictions, ES6 permits other code in a constructor before a
super
call (although accessingthis
in called code would generate a runtime error before super was called). TypeScript is being more strict than what ES6 permits, and sometimes that is a good thing. But, is there any real value in this case by differing from what ES6 allows overall -- compared to just getting in the way? Why not always always allow code before asuper
call when it does not usethis
? Does the benefit of not allowing code before asuper
sometimes really benefit anyone compared to the confusion caused by requiring programmers to use awkward workarounds and to learn a more complex rule for writing constructors than "Don't usethis
before callingsuper
"?This idea was originally brought up in issue #945 (closed in October 2014). I am creating a new issue for that as discussed with @mhegazy here: https://github.com/Microsoft/TypeScript-Handbook/pull/214. There is a code example in that Handbook issue which can be used for testing the current behavior for TypeScript, Babel, and ES6.