Reputation: 744
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)
Thank you
Upvotes: 0
Views: 297
Reputation: 31873
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);
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, i.e. (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.
Upvotes: 2
Reputation: 675
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())
I'm passing the actual class inside super and use it inside getFindId() to return the instance of this class.
Upvotes: -1