Typescript - callback functions get undefined when used from superclass

Viewed 185

I'm rather new to typescript and node.js but I wanted to learn something and try something new with angular, node and express. I still try to find some good practices to split express project into a couple of smaller parts and somehow I got into a problem. I wanted to create an abstract Controller class and some subclasses which would be typical REST controllers backed by express.Router().

Here's the base class - Controller:

import express, {Router} from "express";

export abstract class Controller {
  public router = express.Router();
  public path: string;

  protected constructor(path: string) {
    this.path = path;
    console.log('Calling initializeRoutes() from superclass');
    this.initializeRoutes();
  }
  abstract initializeRoutes(): void;
}

And the subclass - it's just an example - UserController class:

import {Controller} from "./Controller";
import express from "express";

export class UserController extends Controller {

  private users = [
    {name: 'User1', password: 'password1'},
    {name: 'User2', password: 'password2'},
    {name: 'User3', password: 'password3'}
  ]

  constructor(path: string) {
    super(path);
    console.log('Calling initializeRoutes() from subclass');
    this.initializeRoutes();
  }

  initializeRoutes(): void {
    // console.log(this.router);
    console.log(this.getAllUsers);
    console.log(this.createAPost);
    // this.router.get(this.path, this.getAllUsers);
    // this.router.post(this.path, this.createAPost);
  }

  getAllUsers = (request: express.Request, response: express.Response) => {
    response.send(this.users);
  }

  createAPost = (request: express.Request, response: express.Response) => {
    const user = request.body;
    this.users.push(user);
    response.send(user);
  }

The main problem is I would like to call initializeRoutes() in the base class but when I do it express throws error that I can't use undefined callbacks in router. I did some debugging and found that when use initializeRoutes() in the base class callbacks from subclasses are undefined whilst when run from subclasses everything is ok:

[1] [nodemon] starting `ts-node server\server.ts`
[1] Calling initializeRoutes() from superclass
[1] undefined
[1] undefined
[1] Calling initializeRoutes() from subclass
[1] [Function (anonymous)]
[1] [Function (anonymous)]

Why is that? Am I doing something wrong or missing something? Thanks in advance

EDIT: I tried the suggested solution:

export class UserController extends Controller {

  constructor(path: string) {
    super(path);
    this.getAllUsers = this.getAllUsers.bind(this);
    this.createAPost = this.createAPost.bind(this);
    console.log('Calling initializeRoutes() from subclass');
    // this.initializeRoutes();
  }

  initializeRoutes(): void {
    // console.log(this.router);
    this.getAllUsers = this.getAllUsers.bind(this);
    this.createAPost = this.createAPost.bind(this);
    console.log(this.getAllUsers);
    console.log(this.createAPost);
    this.router.get(this.path, this.getAllUsers);
    this.router.post(this.path, this.createAPost);
  }

}

When I put it into initializeRoutes() I get: [1] TypeError: Cannot read property 'bind' of undefined [1] at UserController.initializeRoutes (C:\Blazej\Projects\pasteur\server\controller\UserController.ts:19:41)

When I put it into the constructor I get: [1] Error: Route.get() requires a callback function but got a [object Undefined]

1 Answers
getAllUsers = (request: express.Request, response: express.Response) => {
  response.send(this.users);
}

This syntax is a "class field", which is a feature that's in the process of being added to javascript/typescript, but is not yet quite there yet. You can see the proposal here

But even though it's still making its way through the proposal process, you can use (and are using) it today, thanks to transpilers. Typescript will take the code you've written and turn it into the closest equivalent code. For example, this:

class Example {
  constructor() {
    super();
  }

  someFunction = () => {};
}

Gets turned into basically this:

class Example {
  constructor() {
    super();
    this.someFunction = () => {};
  }
}

But notice that the transpiled code puts the creation of the function after the call to super(). It has to be that way, but that means that while super is running, the function does not yet exist.

So to make the functions exist ahead of time, you'll need to use normal class methods instead of class fields, as in:

getAllUsers(request: express.Request, response: express.Response) {
  response.send(this.users);
}

createAPost(request: express.Request, response: express.Response) {
  const user = request.body;
  this.users.push(user);
  response.send(user);
}

But of course you probably did them as arrow functions on purpose, because you need the value of this to work correctly. So for that, i'd recommend binding the functions. If you don't need the bound functions during initialization you could do it in your child class's constructor:

  constructor(path: string) {
    super(path);
    this.getAllUsers = this.getAllUsers.bind(this);
    this.createAPost = this.createAPost.bind(this);
  }

Or if you do need them during construction, perhaps do it in initializeRoutes

  initializeRoutes(): void {
    this.getAllUsers = this.getAllUsers.bind(this);
    this.createAPost = this.createAPost.bind(this);
    this.router.get(this.path, this.getAllUsers);
    this.router.post(this.path, this.createAPost);
  }
Related