Search code examples
javascripttypescriptexpressclassundefined

Type error cannot read properties of undefined while trying to access a class property created by a class constructor


Im havving trouble building an express api in TS node. Im new at express and im learning Node, JS and TS since 2022 so im sorry if the question is not to complex. The thing is that im tryng to build a class Controller that handles the needed instructions for every route in my express router. And im passing through the constructor a DAO that ive builded to access the firebase firestore database. But when i instanciate the object and i try to run it Gives me the cannot read properties of undefined error. Even when ive found a solution by using a clousure i want to learn how to do this using classes Here it is the code of the DAO

import { setDoc, doc, getDocs, collection, query, where, deleteDoc } from 'firebase/firestore'
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'
import { DataResponse, GenericItem } from '../types'
import { DAO } from '../clases/abstractClasses'
import { v4 } from 'uuid'
import db from '../config/firebase'
// import fs from 'fs/promises'
const fs = require('fs').promises
const storage = getStorage()
export class DataResponseClass implements DataResponse {
  data: GenericItem[]
  status: number
  statusText: string
  err: string
  ok: boolean
  constructor (data: GenericItem[], status: number, statusText: string, err: string, ok: boolean) {
    this.data = data
    this.status = status
    this.statusText = statusText
    this.err = err
    this.ok = ok
  }
}
export class DbManager extends DAO {
  constructor (collectionRef: string) {
    super(collectionRef)
  }

  async addItem (item: GenericItem): Promise<DataResponse> {
    const id = v4()
    console.log(id, typeof id)
    return await setDoc(doc(db, this.collectionRef, id), { ...item, id }).then(res => {
      return new DataResponseClass([{ ...item, id }], 201, 'Item added successfully', '', true)
    }).catch(err => {
      return new DataResponseClass([item], 400, "Couldn't add item", err.toString(), false)
    })
  }

  async getAll (): Promise<DataResponse> {
    return await getDocs(collection(db, this.collectionRef)).then(response => {
      const dataArray: any = []
      response.forEach(item => dataArray.push(item.data()))
      return new DataResponseClass(dataArray, 200, 'Information obtained', '', true)
    }).catch(err => new DataResponseClass([], 400, 'Couldnt Retrieve data', err.toString(), false))
  }

  async getById (passedId: string): Promise<DataResponse> {
    const q = query(collection(db, this.collectionRef), where('id', '==', passedId))
    return await getDocs(q)
      .then(res => {
        const dataArray: any[] = []
        res.forEach(item => {
          dataArray.push(item.data())
        })
        if (dataArray.length === 0) throw new Error('No data found for the id')
        return new DataResponseClass(dataArray, 200, 'Information obtained', '', true)
      })
      .catch(err => new DataResponseClass([], 400, 'Couldnt Retrieve data', err.toString(), false))
  }

  async updateById (id: string, item: GenericItem): Promise<DataResponse> {
    return await setDoc(doc(db, this.collectionRef, id), item)
      .then(() => new DataResponseClass([{ ...item, id }], 200, 'Item succesifuly updated', '', true))
      .catch(err => new DataResponseClass([], 400, 'Couldnt update item', err.toString(), false))
  }

  async deleteByid (id: string): Promise<DataResponse> {
    return await deleteDoc(doc(db, this.collectionRef, id))
      .then(() => new DataResponseClass([], 200, 'Success deleting a document', '', true))
      .catch(err => new DataResponseClass([], 400, 'Couldnt Delete data', err, false))
  }

  async upLoadFile (file: Express.Multer.File | undefined): Promise<string> {
    if (file !== undefined) {
      const buffer = await fs.readFile(file.path).then()
      const reference = ref(storage, `/${this.collectionRef}/${file.filename}`)
      try {
        await uploadBytes(reference, buffer)
        return await getDownloadURL(reference)
      } catch (err: any) {
        console.log(err)
        return 'There was an error uploading the file'
      }
    }
    return 'No file was uploaded'
  }
}

And this is the Controller class code..

import colors from 'colors'
import { Request, Response } from 'express'
import { DbManager, DataResponseClass } from '../services/firebase'
import fs from 'fs/promises'
export class Controller {
  protected readonly dbManager: DbManager
  constructor (collection: string) {
    this.dbManager = new DbManager(collection)
  }

  async readData (req: Request, res: Response): Promise<void> {
    const id: string = req.params.id
    if (id !== undefined) {
      res.send(await this.dbManager.getById(id))
    } else {
      res.send(await this.dbManager.getAll())
    }
  }

  async createData (req: Request, res: Response): Promise<void> {
    if (req.file !== undefined) {
      const uploadedFilePath = await this.dbManager.upLoadFile(req.file)
        .then((response: any) => {
          console.log(`${response}/${req.file?.filename || ' '}`)
          if (req.file?.path !== undefined) {
            fs.unlink(req.file.path).then(() => console.log('Upload Complete')).catch(err => console.log(err))
          }
          return `${response}`
        })
        .catch((err: any) => {
          console.log(err)
          res.send(false)
        })
      const data = { ...req.body, images: uploadedFilePath }
      console.log(colors.bgRed.white(data))
      res.send(await this.dbManager.addItem({ ...req.body, images: uploadedFilePath }))
    } else res.send(new DataResponseClass([], 400, 'Invalid Request no image uploaded', 'Invalid Request no image uploaded', false))
  }

  async editData (req: Request, res: Response): Promise<void> {
    const { id } = req.params
    if (req.file !== undefined) {
      const uploadedFilePath = await this.dbManager.upLoadFile(req.file)
        .then((response: any) => {
          if (req.file?.path !== undefined) {
            fs.unlink(req.file.path).then(() => console.log('Upload Complete')).catch(err => console.log(err))
          }
          return `${response}`
        })
        .catch((err: { toString: () => string }) => {
          console.log(err)
          res.send(new DataResponseClass([], 400, 'Imposible to upload the file', err.toString(), false))
        })
      res.send(await this.dbManager.updateById(id, { ...req.body, images: uploadedFilePath }))
    } else res.send(new DataResponseClass([], 400, 'Invalid Request no image uploaded', 'Invalid Request no image uploaded', false))
  }

  async deleteData (req: Request, res: Response): Promise<void> {
    const { id } = req.params
    if (id !== undefined) {
      res.send(await this.dbManager.deleteByid(id))
    } else res.send(new DataResponseClass([], 400, 'Invalid Request no id', 'Invalid  Request no id', false))
  }
}

Thanks for your time Im tryng to learn this beautifull world that is the backend development

Ive tryed to call the constructor outside the class and pass the constant to the constructor Ive tryed to instanciate the DAO object as a param,Ive even called the dao constructor in a global variable and defined the properti taking value from it . But the only solution ive found for my issue es transforming the class into a clousure function and calling the constructor in the body of the closure

TypeError: Cannot read properties of undefined (reading 'dbManager')
    at /run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:20:27
    at Generator.next (<anonymous>)
    at /run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:8:71
    at new Promise (<anonymous>)
    at __awaiter (/run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:4:12)
    at readData (/run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:24:16)
    at Layer.handle [as handle_request] (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/layer.js:95:5)
    at next (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/route.js:144:13)
    at Route.dispatch (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/route.js:114:3)
    at Layer.handle [as handle_request] (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/layer.js:95:5)
[ERROR] 16:55:20 TypeError: Cannot read properties of undefined (reading 'dbManager')

Routes Here I call the method readData

import { Router } from 'express'
import { Controller } from '../controllers/controllerClass'
// import { Validation } from '../services/validation'
import { upload } from '../config/multer'
const router = Router()
// const { validate } = new Validation('welcome')
const { readData, createData, editData, deleteData } = new Controller('welcome')
router.get('/', readData)
router.get('/:id', readData)
router.post('/', upload.single('images'), createData)
router.put('/:id', upload.single('images'), editData)
router.delete('/:id', deleteData)
export default router

Solution

  • const { readData, createData, editData, deleteData } = new Controller('welcome')
    

    You can't destructure normally declared instance methods from classes.


    I'm going to vastly simplify this to this example:

    class Foo {
        private data = 123
        getData() { return this.data }
    }
    

    Now if you call getData like so, it works:

    const foo = new Foo()
    console.log(foo.getData())
    // 123
    

    But if you destructure the method:

    const { getData } = new Foo()
    console.log(getData())
    // Cannot read properties of undefined (reading 'data')
    

    Then it crashes.

    The value of this is being lost because you don't call it with a ., which is what provides the class instance to the function.


    Now let's try this one:

    class Foo {
        private data = 123
        getData = () => { return this.data }
    }
    
    const foo = new Foo()
    console.log(foo.getData())
    // 123
    
    const { getData } = new Foo()
    console.log(getData())
    // 123
    

    This works because typescript compiles property assignments in classes to happen in the constructor, and because the arrow function => captures the value of this from when it was declared. So now you can break off the method and it works.

    Just note that while the traditional instance method declaration is shared between all instances, this arrow function method will create a new function for every instance. This may hurt performance if you plan to create a very large number of instances. But for backend service classes like this that's probably not a concern.


    So in your case just call the method on the instance:

    const controller = new Controller('welcome')
    router.get('/', (req, res) => controller.readData(req, res))
    

    Or you declare your method as a arrow function.

    export class Controller {
      //...
      readData = async (req: Request, res: Response): Promise<void> => {
        const id: string = req.params.id
        if (id !== undefined) {
          res.send(await this.dbManager.getById(id))
        } else {
          res.send(await this.dbManager.getAll())
        }
      }
      //...
    }
    
    const { readData } = new Controller()
    readData() // fine now