Skip to content
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

Always allow code before super call when it does not use "this" #8277

Closed
pdfernhout opened this issue Apr 24, 2016 · 44 comments
Closed

Always allow code before super call when it does not use "this" #8277

pdfernhout opened this issue Apr 24, 2016 · 44 comments
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@pdfernhout
Copy link

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.

It is reasonable in TypeScript to not permit this to be referenced in a constructor before calling super when there are initialized properties or constructor parameter properties because this is not fully initialized until after super is called. But broader restrictions on calling other code before super that is not directly using this 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 a super call such as super(logThisName(name)) where the called function refers to this.

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 accessing this 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 a super call when it does not use this? Does the benefit of not allowing code before a super 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 use this before calling super"?

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: microsoft/TypeScript-Handbook#214. There is a code example in that Handbook issue which can be used for testing the current behavior for TypeScript, Babel, and ES6.

@electricessence
Copy link

electricessence commented Apr 25, 2016

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.. :/

@iby
Copy link

iby commented May 3, 2016

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.

@mhegazy mhegazy added Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". and removed In Discussion Not yet reached consensus labels May 17, 2016
@DanielRosenwasser
Copy link
Member

It pretty much looks like this is fixed for 2.0 beta. Can you give it a try @pdfernhout?

@martinsik
Copy link

martinsik commented Oct 26, 2016

@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);
  }
}

@captainjono
Copy link

captainjono commented Mar 30, 2017

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 {
}

}`

@davidglezz
Copy link

davidglezz commented Sep 30, 2017

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
        })())
}

@marlon-tucker
Copy link

marlon-tucker commented Dec 14, 2017

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?

@electricessence
Copy link

@davidglezz this is exactly what I've had to do.

@mhegazy mhegazy added this to the Community milestone Jan 4, 2018
@pdfernhout
Copy link
Author

pdfernhout commented Feb 22, 2018

@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);
    }
}

@Kingwl
Copy link
Contributor

Kingwl commented May 4, 2018

@mhegazy hi
what kind of pr that this issue accept😅

JoshuaKGoldberg pushed a commit to JoshuaKGoldberg/TypeScript that referenced this issue Jan 11, 2019
Fixes microsoft#8277.

It feels wrong to put a new `forEachChild` loop in the checker, though in the vast majority of user files this will be a very quick one. Is there a better way to check for a reference to `super` or `this`?
@JoshuaKGoldberg
Copy link
Contributor

JoshuaKGoldberg commented Jan 14, 2019

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!):

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 #29374 (comment)

Related but not the same: #23422 on the subject of multiple super()s.

@chharvey
Copy link

chharvey commented May 20, 2020

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 (#8277 (comment), #8277 (comment)), 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.

@pschiffmann
Copy link

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.

@chharvey
Copy link

chharvey commented Sep 10, 2020

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

@fprott
Copy link

fprott commented Dec 1, 2020

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.

@ghost
Copy link

ghost commented Jan 19, 2021

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.

@Jamesernator
Copy link

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 reference this.

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.

@chharvey
Copy link

chharvey commented Feb 27, 2021

@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.

@mysticatea
Copy link

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.

@jimmywarting
Copy link
Contributor

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)

@isometriq
Copy link

isometriq commented May 30, 2021

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.

@isometriq
Copy link

isometriq commented May 30, 2021

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
}

@ghost

This comment has been minimized.

@isometriq
Copy link

isometriq commented May 31, 2021

@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.

@mysticatea
Copy link

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.

@ghost
Copy link

ghost commented May 31, 2021

@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.

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.

@Hibou57
Copy link

Hibou57 commented Jun 8, 2021

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 …

@nwetzel22
Copy link

nwetzel22 commented Feb 25, 2023

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.

@RyanCavanaugh
Copy link
Member

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.

@nwetzel22
Copy link

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.

jashkenas/backbone#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.

@DanielRosenwasser
Copy link
Member

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.

@dcporter
Copy link

dcporter commented Mar 1, 2023

@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

@nwetzel22
Copy link

@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.

@dcporter
Copy link

dcporter commented Mar 1, 2023

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 😄🤞🏻

@nwetzel22
Copy link

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.

Example

@dcporter
Copy link

dcporter commented Mar 1, 2023

Fantastic news that I should have gotten previously and on my own. Thanks! Updated my OP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests