I got really lucky with the fix for this one, as it had me scratching my head for a good while. The gist of this is that I had two classes – a base class, and a sub class. I wanted to call a method with the same name on the sub class as exists on the base class, but ended up with the TS2340 error.
The fix is down to the way the functions were defined. If your function is defined as a class property (an arrow function) then you’re most likely going to hit the same error I did. So the fix is to use the regular function format.
If that makes no sense, or you’re looking for a little more info, keep reading.
Basic Setup
Here’s some example code to get us started:
class A {
hello = () => {
console.log('A');
}
}
class B extends A {
hello = () => {
console.log('B');
}
}
const a = new A();
a.hello();
const b = new B();
b.hello();
Code language: TypeScript (typescript)
Pretty straightforward. Two classes, both define a hello
method which console logs out the respective class name as a hardcoded string:
➜ ts-super-example npx tsc src/super-testing.ts && node src/super-testing.js
A
B
Code language: JavaScript (javascript)
Call The Base / Parent / Super Class
What I wanted was to be able to call b.hello
and get A
as output.
How to do this?
Well, call through to the base class, right?
And how do we do that?
Using the super
keyword:
class B extends A {
hello = () => {
super.hello()
}
}
Code language: TypeScript (typescript)
But this no longer compiles:
This is not the best error message, in my humble opinion.
Firstly, by default, methods are public on TypeScript classes.
Secondly, changing the accessibility modifier doesn’t solve the problem.
If we swap the hello
method in A
to be protected
, we can no longer call this method from outside of that class. That’s not what we want.
Equally if we change the code as follows:
class A {
public hello = () => {
console.log('A');
}
}
const a = new A();
a.hello();
Code language: TypeScript (typescript)
Then we have only made the public
acccessibility modifier explicit. We haven’t solved the problem because it is implicitly public
. Which is to say whether we use the public
keyword or not, the method is public
.
So… yeah, what gives?
The Fix
As above, the fix is to do with the way in which the hello
function is defined.
By sheer chance, I stumbled across another use of super
in one of the other projects I have access too that gave away the fix. Thank goodness!
Here’s the change we need to make:
class A {
hello() {
console.log('A');
}
}
class B extends A {
hello = () => {
super.hello()
}
}
const a = new A();
a.hello();
const b = new B();
b.hello();
Code language: TypeScript (typescript)
We have to change line 2 to use the regular function definition syntax. Or to put it another way, line 2 cannot be an arrow function.
Interestingly, line 8 – the sub class definition of the function that overrides the base class function – can be either an arrow function or a regular function. I found that surprising.
However, we will cover this is more below, and you should probably not use an arrow function here:
class A {
hello() {
console.log('A');
}
}
class B extends A {
hello() {
super.hello()
}
}
Code language: TypeScript (typescript)
This code now compiles, runs, and outputs the expected:
➜ ts-super-example npx tsc src/super-testing.ts && node src/super-testing.js
A
A
Code language: JavaScript (javascript)
As best I am aware, this is to do with what is actually happening under the hood when the code is compiled from TypeScript down to JavaScript.
If we look at the MDN web docs for Arrow Functions the very first bullet point gives the game away (emphasis mine):
An arrow function expression is a compact alternative to a traditional function expression, with some semantic differences and deliberate limitations in usage:
MDN Docs for Arrow function expressions
- Arrow functions don’t have their own bindings to
this
,arguments
, orsuper
, and should not be used as methods.- Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
. They also don’t have access to thenew.target
keyword.- Arrow functions cannot use
yield
within their body and cannot be created as generator functions.
So, err… whoops.
That actually made basic sense to me… until, that is, I tried out the variant above whereby I was still able to use super
inside the B.hello
function definition.
For that, I was truly stumped. However ChatGPT offers this by way of explanation:
Over To ChatGPT For Some Sage Advice
In your example, B
extends A
, and B.hello
is an arrow function that calls super.hello()
. When you call b.hello()
, the arrow function is invoked, and because arrow functions don’t bind this
, arguments
, or super
, the value of this
inside the arrow function is actually the value of this
in the lexical scope where the arrow function was defined. In this case, this
is the instance of B
that you created with new B()
.
When you call super.hello()
inside the arrow function, JavaScript looks up the hello
method on the prototype chain of the current object (b
), which is B.prototype
. Because B
extends A
, B.prototype
also has access to the hello
method defined on A.prototype
. When super.hello()
is called, it calls the hello
method on the superclass (A.prototype.hello
) with this
set to the instance of B
(b
).
So, in summary, even though arrow functions don’t have their own bindings to super
, in the specific case where you’re defining a class method on a subclass using an arrow function and calling super
inside the method, it works because the super
reference is resolved dynamically based on the prototype chain, and the this
value is set to the instance of the subclass. However, this is generally not a recommended way of defining class methods, because it can be confusing and harder to reason about than using traditional method syntax.
Wrapping Up
Firstly then, the fix is superficially pretty simple: don’t use arrow functions if using super
.
There’s actually a bit more to it than that.
Arrow functions could potentially be less performant than regular functions as a new function is created for every instance of the class.
However, arrow functions make working with this
inside methods far easier to reason about. There’s no need to think about bind()
or self
with arrow functions, which can only be a good thing.
So, as ever with JavaScript, there’s a lot going on under the hood that can be quite confusing. I think this is why I tend not to use classes with JavaScript (or TypeScript) if I have the choice.