简体   繁体   中英

TypeScript inheritance static methods cast

I have a TypeScript project with two classes BaseModel and HotelModel. The HotelModel extends the BaseModel class that provides some static methods like findById, all, etc..

export default class BaseModel {

  private collection:string
  _id:string | undefined

  constructor (collection:string) {
    this.collection = collection
  }

  static getCollectionName () {
    return this.prototype.constructor.name.toString().toLowerCase() + 's'
  }

  static async findById (id:string) {
    const connection = await getConnection()
    const hotel = await connection.collection(this.getCollectionName())
    .findOne({
      _id: new mongodb.ObjectId(id)
    })
    if (!hotel) {
      throw new ResourceNotFound('Hotel not found with the given id' + id)
    }
    return hotel
  }

}

and this is the HotelClass

import BaseModel from './baseModel'
import IHotel from '../interfaces/IHotel'

import ValidationException from '../../exceptions/ValidationException'

export default class Hotel extends BaseModel {

  name:string
  status:string
  metadata:object

  constructor (hotel:IHotel) {
    super('hotels')
    this.name = hotel.name
    this.status = hotel.status
    this.metadata = hotel.metadata
  }

  validate () {
    if (!this.name || this.name === '') {
      throw new ValidationException('Name field is required')
    }
  }

}

Now when i call HotelModel.findById(1) i would like to receive back an istance of the member class (HotelModel) is this possible? How can i achieve that?

------UPDATE------

based on suggestion this is what i got

export default class Service<T> {

  private collection:string

  constructor (collection:string) {
    this.collection = collection
  }

  async findById (id:string) {
    const connection = await getConnection()
    const model = await connection.collection(this.collection)
      .findOne({
        _id: new mongodb.ObjectId(id)
      }) as T
    if (!model) {
      throw new ResourceNotFound('Model not found with the given id' + id)
    }
    return model
  }

}

then i have a HotelService class that extends the generic one and inherits all the methods

export default class HotelService extends Service<HotelModel> {

  public constructor () {
    super('hotels')
  }

}

------UPDATE 2------

Well, it took quite a lot of time but I found an "elegant" (at least to me) solution to solve the problem

class QueryBuilder {

    private modelType: typeof BaseModel;

    constructor (modelType: typeof BaseModel) {
        this.modelType = modelType
    }

    data:Array<any> = [
        { id: '1', name: 'Jane' },
        { id: '2', name: 'John' },
        { id: '3', name: 'Mark' }
    ]

    findById (id:string) {
        // fake database call
        const data = this.data.find(r => r.id === id)
        // "cast" the database object to the required type
        let model:any = new this.modelType()
        model.fill(data)
        return model
    }
    
}

class BaseModel {

    private id:string | undefined

    constructor () {}

    static findById () {
        return new QueryBuilder(this)
            .findById('1')
    }

    public save () {
        console.log('calling save')
        this.id = '123456'
    }

    public fill (data:any) {
    }

}

class HotelModel extends BaseModel {

    public name:string | undefined

    constructor (
        name:string
    ) {
        super()
    }

}

let h:HotelModel = HotelModel.findById()
h.name = 'test name'
h.save()
console.log(h)
console.log(h instanceof HotelModel)

PlayGround

Thank you

I believe this is what you are after

export default class BaseModel {
  collection: string
  _id: string | undefined

  constructor(collection: string) {
    this.collection = collection;
  }

  static get collectionName() {
    return this.name.toLowerCase() + 's';
  }

  static async findById<T extends BaseModel>(
    this: (new (...args: any[]) => T) & Pick<typeof BaseModel, keyof typeof BaseModel>,
    id: string
  ): Promise<T> {
    const connection = await getConnection();
    const model = await connection.collection(this.collectionName)
      .findOne({
        _id: new mongodb.ObjectId(id)
      });
    if (!model) {
      throw new ResourceNotFound(`${this.collectionName} not found with the given id ${id}`);
    }
    return model as T;
  }
}

export default class Hotel extends BaseModel { ... }

const hotel = await Hotel.findOneBy('1');
console.log(hotel.name);
console.log(hotel.status);

Playground Link

So, what's going on here?

We are using using TypeScript's ability to specify the type of the this value that functions and methods implicitly receive.

Since we are in a static method, the this type refers to the type of the class itself. That type is something we can call with new which is to say it is a constructor.

However, we want to capture the actual type of the derived class. To this end, we declare a generic type, T , that represents whatever a derived class returns when we call it with new . Then we state that this is a constructor that creates T s. We've lost access to the static members of the base class in doing so, however, and we have to add them back in with an intersection.

Finally, when we call Hotel.findById , TypeScript infers T from typeof Hotel because typeof Hotel is the type of the value findById is being called on.

Note: Normally, the this type for findById would be simpler to write, ie (new (...args: any[]) => T) & typeof BaseModel but in this case, your derived class Hotel has a constructor with an incompatible argument list. I used Pick<typeof BaseModel, keyof typeof BaseModel> as a quick and dirty way to obtain a type containing all of the members of typeof BaseModel except call and construct signatures.

Overload static function of Hotel

  static async findById (id:string) {
    const data = await BaseModel.findById(id)
    return new Hotel(data)
  }

I'm not used to typescript so maybe someone can help me but following your update I think you need to pass the actual constructor value and not just a type

Here is an example

class Service {
   private collection: string
   private Model: any

   constructor (Model: any, collection: string) {
       this.collection = collection
       this.Model = Model
   }

   findById (id:string) {
    console.log(this.collection)
    return new this.Model(id)
  }
}

class HotelModel {
    public id: string
    constructor (id: string) {
        this.id = id
    }

    test () {
        return '1'
    }
}

class HotelService extends Service {
    constructor () {
        super(HotelModel, 'hotels')
    }
}

const hotelService = new HotelService()
const hotel = hotelService.findById('1')

console.log(hotel.test())

Playground

I'm passing the actual class inside super and use it inside getFindId() to return the instance of this class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM