Assign a function in one class to be equivalent to a function in a different class

What is causing this issue and how should it be resolved?

class A {
    constructor() {
        console.log('constructin A')
    }
  public someMethod = (x: string) => {
    console.log(x)
    }
}

class B {
    private myA: A
    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }
    // Fails with "Uncaught TypeError: Cannot read property 'someMethod' of undefined"
    anotherMethod = this.myA.someMethod;
}

const a = new A()
const b = new B(a)

b.anotherMethod('hello')

typescript playground link

Additional Information:

  • This issue persists even when using plain JavaScript instead of TypeScript.
  • The preference for the syntax
    anotherMethod = this.myA.someMethod
    over
    anotherMethod = (x: string) => { this.myA.someMethod(x) }
    stems from complex type annotations on someMethod that are impractical to duplicate on anotherMethod.

UPDATE (19.06.2020) - 'with decoupling'

The explanations provided in response to this query, including comments such as this, shed light on why the original syntax fails to function. Regarding potential solutions:

Decoupling A and B: While not explicitly stated initially, the objective behind having B utilize a method from A is to establish a level of separation between them. In practical scenarios, different instances of A adhere to a common interface comprising someMethod implementations tailored to their specific requirements. The desired outcome is for B to interact with someMethod offered by any given A instance without delving into implementation nuances. The accepted resolution comprehends this intention effectively through detailed code examples. It presents an optimal, seamless syntax devoid of pitfalls for this particular scenario. For quick reference, refer to the accepted answer, specifically:

public anotherMethod(...args: Parameters<A['someMethod']>): ReturnType<A['someMethod']> {
    return this.myA.someMethod(...args);
}

In cases where decoupling is unnecessary, this outlined approach may prove adequate.

Answer №1

Identifying the root cause of the issue:

The issue arises from the fact that

anotherMethod = this.myA.someMethod;
is executed prior to this.myA = a;. The JavaScript code generated by the TypeScript compiler looks like this:

class B {
    constructor(a) {
        this.anotherMethod = this.myA.someMethod;
        console.log('constructin B');
        this.myA = a;
    }
}

As a result, an error is inevitable in this scenario.

If you need to link the method during construction, you must manually do so 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

Here's how it would 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 more effective solution

In the comments, the OP mentioned:

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 scenarios, I recommend avoiding direct method attachments and instead defining a single method in B that forwards 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 approach is cleaner than assigning methods to instances during construction. It clearly specifies the parameters and return types expected from someMethod. Even if someMethod returns void, it remains valid.

Here's how it can be implemented using an interface for A and separate classes implementing it:

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

You can skip reading further from here.


(Optional continuation) Reasons behind the suggested solution

Let's delve into the rationale for choosing a method-based delegation over property assignment within B. This exploration uncovers potential pitfalls associated with different approaches.

...continued explanation of various advanced concepts and potential issues...
  • Detailed examination of shadowing this context using functions and getters
  • Exploration of arrow functions vs regular methods regarding context retention
  • Analysis of using Proxy objects for context preservation

Answer №2

anotherMethod will have a definition prior to setting myA in the constructor. An effective solution for your issue is to utilize a getter:

get anotherMethod() {
    return this.myA.someMethod;
}

Interactive Coding Playground

In addition, you can opt to make the method within A static. This would eliminate the necessity of passing an instance of A to B. Ensure that B has access to A.

class A {
    constructor() {
        console.log('constructin A')
    }

    static someMethod = (x: string) =>{
        console.log(x)
    }
}

class B {
    private myA: A
    constructor(a: A) {
        console.log('constructin B')
        this.myA = a
    }

    anotherMethod = A.someMethod;
}

const a = new A()
const b = new B(a)

b.anotherMethod("Hello")

Online Code Editor

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unable to locate element in Internet Explorer when using frame switching within Selenium

I am currently working on a project in Selenium that is specifically designed to run in Internet Explorer, but I am encountering issues locating the xpath element. Despite this setback, I managed to make progress in the test by using the switch frame func ...

Struggling to implement touch event functionality for CSS3 spotlight effect

I'm experimenting with implementing touch events on an iPad to achieve a certain effect. Currently, I have it working smoothly with mouse events on JSFiddle at this link: http://jsfiddle.net/FwsV4/1/ However, my attempts to replicate the same effect ...

What is the process for sending a post request in the inline editor of Dialogflow?

Currently, I am utilizing the blaze tier, so there should be no billing concerns. I have also added "request" : "*" in my package.json dependencies. Check out my code index.js below: ` 'use strict'; var global_request = require('requ ...

Create a dynamic menu dropdown with absolute positioning using React

I recently made the transition to React and now I am in the process of converting my old vanillaJS website into ReactJS. One issue I am facing is with a button that is supposed to trigger the opening of a dropdown menu. <button type="button&qu ...

Issues encountered with JavaScript when trying to use the jQuery API

I'm trying to fetch random quotes using a Mashape API, but when I click the button, nothing happens. I've included the JS and HTML code below. Can anyone spot an issue with the JS code? The quote is not displaying in the div. Thank you! $(' ...

Every other attempt at an Ajax request seems to be successful

I'm new to using ajax and I'm having an issue with submitting a form through a post request. Strangely, the code I wrote only works every other time. The first time I submit the form, it goes through ajax successfully. However, on the second subm ...

Expo constants failing to load on web due to unresolved manifest object issue

When setting up Firebase Auth in my expo app (using Google Auth), I needed to store my firebase variables in a .env file containing API_KEYS, AuthDomain, and more. To access these environment variables, I utilized expo constants in my firebase.ts file. Ini ...

What is the reason for the directive being available in $rootScope?

Currently, there doesn't seem to be a major issue but it has sparked my curiosity. I have a straightforward directive that, for some unknown reason, is accessible within $rootScope. JAVASCRIPT: (function(){ var app = angular.module('myAp ...

Set the array as the object attribute

Transitioning my app from AngularJs to Angular 4 has been quite a challenge. I've noticed that the type of statements I frequently used in my code are now failing in Angular 4 (TypeScript): Update: The following lines were previously used in Angular ...

"Endowed with improper dimensions, the BootStrap collapse feature

Yesterday, I posted about an issue with BootStrap and panel collapsables causing graph sizes to become distorted. The post was locked because there was no accompanying code. I have now created a code snippet for you all to see the exact problem I am facing ...

Using Typescript: What is the best way to convert a variable into a specific element of an array?

List of Strings: const myStrings = ["one", "two", "three"]; const newString = "two"; The variable newString is currently just a string, but I would like its type to be an element of myStrings. Is there a way to achi ...

MeteorJS: Verification of User Email addresses

After sending an email verification to a user, how can I ensure they actually verify their email after clicking on the link sent to their address? I'm aware of this function Accounts.onEmailVerificationLink but I'm unsure of how to implement i ...

Steps to ensure that a particular tab is opened when the button is clicked from a different page

When I have 3 tabs on the register.html page, and try to click a button from index.html, I want the respective tab to be displayed. Register.html <ul class="nav nav-tabs nav-justified" id="myTab" role="tablist"> <l ...

Encountering a problem with Firebase while offline. The error message "FirebaseError: Firebase App named '[DEFAULT]' already exists with different options or config." is appearing

I've been having some trouble integrating Firebase into my app using the useFireBaseAuth hook. Everything works smoothly when there's an active internet connection, but I'm facing issues when offline. An error message shows up: Server Error ...

I am experiencing issues with my Jest unit test case for Material UI's multi select component failing

I've been working on writing a unit test case for the material UI multi select component. The code for the parent component is as follows: import {myData} from '../constant'; export const Parent = () => { const onChangeStatus= (sel ...

Dynamic Search Functionality using Ajax and JavaScript

function fetchData(str) { if (str.length == 0) { var target = document.querySelectorAll("#delete"); return; } else { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (this.readyState == 4 && ...

Using jQuery to load HTML response into entire page

When working with my ajax code, I receive a html response. Is there a way to entirely replace the current page with this html response? So far, I have only come across window.location.href, which simply redirects to the url response. Here is a snippet of ...

Troubleshooting the 'App Already Bootstrapped with this Element' Error in AngularJS

When I try to load my AngularJS app, I encounter the following error: Uncaught Error: [ng:btstrpd] App Already Bootstrapped with this Element '<html lang="en" ng-app="app" class="ng-scope">' I have only placed ng-app once in the html elem ...

What steps can be taken to restrict users to providing only one comment and rating for each item?

In the backend controller, I have the following code snippet: 'use strict'; var Comment = require('../../../models/comment'); module.exports = { description: 'Create a Comment', notes: 'Create a comment&apos ...

Error occurs when attempting to read the 'map' properties of null because the JSON array is double nested

Within my code, I am attempting to access the URLs of two thumbnails in the JSON data below. Currently, I can only retrieve the information from the first array: <>{post.attributes.description}</> However, I am encountering difficulty retrievi ...