The reason you experience an issue:
The problem is that anotherMethod = this.myA.someMethod;
is executed before this.myA = a;
. Here is the JavaScript code that the TypeScript compiler produces:
class B {
constructor(a) {
this.anotherMethod = this.myA.someMethod;
console.log('constructin B');
this.myA = a;
}
}
So, there is no way for this not to produce an error.
If you need to attach the method at construction time, you need to do it yourself in the constructor:
class A {
constructor() {
console.log('constructin A')
}
public someMethod = (x: string) => {
console.log(x)
}
}
class B {
private myA: A
//declare the anotherMethod interface to match the someMethod interface
public anotherMethod: typeof A.prototype.someMethod
constructor(a: A) {
console.log('constructin B')
this.myA = a
//assign the method. Note: `this.anotherMethod = a.someMethod` is equivalent
this.anotherMethod = this.myA.someMethod;
}
}
const a = new A()
const b = new B(a)
b.anotherMethod('hello') //OK
b.anotherMethod(42) //error - does not accept numbers
Playground Link
And here is how this can look in pure JavaScript
class A {
constructor() {
console.log('constructin A')
}
someMethod = (x) => {
console.log(x)
}
}
class B {
constructor(a) {
console.log('constructin B')
this.myA = a;
this.anotherMethod = this.myA.someMethod;
}
}
const a = new A()
const b = new B(a)
b.anotherMethod('hello')
A better solution
In comments the question asker said:
I wanted separation of concerns, so that the implementation details of calling the api is delegated to the A
classes, and B
is just composed with a specific A
.
For such a use case, I would suggest not directly attaching the methods but instead just declare a single method in B
that delegates all calls to myA
:
//method with the same signature as A.someMethod
public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
//use the delegate for the call
return this.myA.someMethod(...args);
}
This is much cleaner than assigning the methods to instances at construction time. The ...args: Parameters<A['someMethod']>
will always be equal to the parameters that someMethod
takes. Conversely, ReturnType<A['someMethod']>
resolves to the type that someMethod
returns. If that is void
, then it's still valid.
Here is how this can look - I've turned A
into an interface that is implemented by different classes. B
now only knows about the interface and doesn't care how it's implemented:
interface A {
someMethod(x: string): void;
}
class X implements A {
constructor() {
console.log('constructin X')
}
public someMethod = (str: string) => {
console.log("X", str);
}
}
class Y implements A {
constructor() {
console.log('constructin Y')
}
public someMethod = (str: string) => {
console.log("Y", str);
}
}
class Z implements A {
constructor() {
console.log('constructin Z')
}
public someMethod = (str: string) => {
console.log("Z", str);
}
}
class B {
private myA: A
constructor(a: A) {
console.log('constructin B')
this.myA = a
}
public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
return this.myA.someMethod(...args);
}
}
const x = new X()
const y = new Y()
const z = new Z()
const b1 = new B(x)
const b2 = new B(y)
const b3 = new B(z)
b1.anotherMethod('hello') //OK
b2.anotherMethod('hello') //OK
b3.anotherMethod('hello') //OK
b1.anotherMethod(42) //error - does not accept numbers
Playground Link
You can stop reading here.
(Optional reading to the end) Why I suggest this solution
I would like to address why using a method for delegation works better as opposed to using in B
. This is going in depth with potential pitfalls.
get anotherMethod() {
return this.myA.someMethod;
}
In the above case the getter will always return a function reference. This allows for calling
b.anotherMethod("hello")
and this will compile correctly, it will have the correct method signature, and can be called. However, there is a subtle yet very widely encountered problem the this
context will be lost. Recommended reading:
How does the "this" keyword work?
Here is the short version - the value for this
is determined at the time of calling a function. To keep it simple, here is a rule of thumb: the value is whatever is before the final dot when calling:
//calling quux()
foo.bar.baz.quux() // -> this = foo.bar.baz
^^^^^^^^^^^ ^^^^^^^^^^^
| |
---------------------------------
//calling baz()
foo.bar.baz() // -> this = foo.bar
^^^^^^^ ^^^^^^^
| |
----------------------------
//calling bar()
foo.bar() // -> this = foo
^^^ ^^^
| |
------------------------
//calling foo()
foo() // -> this = undefined
^
|
nothing
For more information, I suggest reading the links I have provided.
This is important because the call is what determines this
. A very common problem in JavaScript is losing the context by doing the following:
"use strict";
const foo = {
value: 42,
bar() {
return this.value;
}
}
console.log(foo.bar()); //this = foo; foo.value = 42
//get a function reference
const func = foo.bar;
console.log(func()); // this = undefined; undefined.value = error
This is the very basic form of the error. It can manifest in many different ways, such as when passing callbacks:
functionThatTakesCallback(foo.bar);
and it's going to manifest when the get
is used. It's actually more insidious, since you would call the function but with a new context:
class B {
private myA: A
/* simplified for brevity */
get anotherMethod() {
return this.myA.someMethod;
}
}
const b = new B(a);
b.anotherMethod("hello"); // this = b;
Now there is no way to ensure that anotherMerhod
(which is an alias for someMethod
) will work correctly.
If someMethod
doesn't use any instance data (no this
inside it), then it will work. But there is no way to verify that.
If someMethod
is a regular method (or function - the difference is negligible here) and uses this
then calling b.anotherMethod("hello")
is almost guaranteed to produce the wrong result or even an error.
If someMethod
is an "arrow method" (will go in details below) then the this
context will be automatically bound and thus the result is correct. Again, there is no convenient way to verify that.
What is an "arrow method"? It's this construct:
class X implements A {
private myProp: string;
/* simplified for brevity */
public someMethod = (str: string) => { //"arrow method"
console.log(this.myProp, str);
}
}
In reality, this is an an arrow function assigned as a class property. The difference is that it's automatically added to the instance and this
would be lexically bound inside, thus it is always going to point to the current instance.
By contrast, a regular method looks like this:
class Y implements A {
private myProp: string;
/* simplified for brevity */
public someMethod(str: string) { //regular method
console.log(this.myProp, str);
}
}
It is shared between all instances as it exists on the prototype. This ensures that there is only one copy of this function in memory, the arrow function class property creates one for each instance of X
created. This doesn't need to be a problem, but if there are going to be a lot of X
objects created, it leads to increased memory usage. The drawback of Y
is the potential loss of this
. So, it's a bit of a balancing act.
Here is an example of how things can go wrong:
interface A {
someMethod(x: string): void;
}
class X implements A {
//instance property
private myProp: string;
constructor(name: string) {
console.log('constructin X')
this.myProp = name;
}
public someMethod = (str: string) => { //"arrow method"
console.log(this.myProp, str);
}
}
class Y implements A {
//instance property
private myProp: string;
constructor(name: string) {
console.log('constructin Y')
this.myProp = name;
}
public someMethod(str: string) { //regular method
console.log(this.myProp, str);
}
}
class B {
private myA: A
constructor(a: A) {
console.log('constructin B')
this.myA = a
}
get anotherMethod() {
return this.myA.someMethod;
}
}
const x = new X("Foo")
const y = new Y("Bar")
const b1 = new B(x)
const b2 = new B(y)
b1.anotherMethod('hello'); //Foo hello
b2.anotherMethod('hello'); //undefined hello
//this.myProp is taken from B. Illustration:
(b2 as any).myProp = "This is B"
b2.anotherMethod('hello') //This is B hello
Playground Link
With all this said, there is a way to patch the get
approach. The value of this
can be permanently set with Function#bind
class B {
private myA: A
/* simplified for brevity */
get anotherMethod() {
return this.myA.someMethod.bind(this.myA);
}
}
The problem now is that .bind()
returns a new function every time. So
b.anotherMethod("hello");
b.anotherMethod("hello");
will have the correct value for this
but this actually creates two functions, executes them, then discards them. In this case memory isn't a problem, as the functions will simply be garbage collected, but garbage collection itself might be an issue. A code that calls the getter many times, for example like this:
for(let i = 0; i < 9000; i++) {
b.anotherMethod("hello");
}
will lead to many runs of the garbage collector which can hamper performance. Again, not necessarily a problem but something that needs to be mentioned.
Another potential issue is if you ever need to compare functions for equality
b.anotherMethod === b.anotherMethod //false
Playground Link
Every b.anotherMethod
produces a different function. It is rare that you would need to compare functions but it might come up if you use a Set or a Map, for example:
const set = new Set();
set.add(b.anotherMethod);
//later
set.has(b.anotherMethod); //false
I will just mention an even more complex way of making sure the get
works is keeping
get anotherMethod() {
return this.myA.someMethod;
}
and then using a Proxy to wrap X
instances and intercept any invocations of anotherMethod
and instead execute is using Function.call
or Function#appy
in order to preserve the context.
However, in my view these are all solutions that have issues of their own. Especially the Proxy approach is very much overengineering. This is why I prefer a simple method that delegates the call to this.myA
:
public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
return this.myA.someMethod(...args);
}
- it's easy to read - you know exactly what's being done.
- easy to understand - the intention is clear
- there are no hidden drawbacks:
this.myA.someMethod(...args)
will always use this.myA
as the value of this
for someMethod
regardless of how that method is defined.
- it's a regular method thus there is only one copy of it in memory.