#javascript #node.js #typescript
#javascript #node.js #typescript
Вопрос:
У меня есть проект TypeScript с двумя классами BaseModel и HotelModel. HotelModel расширяет класс BaseModel, который предоставляет некоторые статические методы, такие как findById, all и т.д..
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
}
}
и это 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')
}
}
}
Теперь, когда я вызываю HotelModel.findById(1), я хотел бы получить обратно значение класса-члена (HotelModel), возможно ли это? Как я могу этого добиться?
——ОБНОВИТЬ——
основываясь на предложении, это то, что я получил
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
}
}
тогда у меня есть класс HotelService, который расширяет общий класс и наследует все методы
export default class HotelService extends Service<HotelModel> {
public constructor () {
super('hotels')
}
}
——ОБНОВЛЕНИЕ 2——
Ну, это заняло довольно много времени, но я нашел «элегантное» (по крайней мере, для меня) решение для решения проблемы
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)
Спасибо
Комментарии:
1. Вы можете использовать
return new this(found);
. Однако TypeScript может отказаться вводить это правильно. Я бы предложил использовать отдельныйService<Hotel>
или (Store
или как вы хотите его назвать), который можно сделать универсальным, вместо статических методов.2. Предложение Берги — это то, как я бы это сделал.
3. @Bergi Хорошо! Я думаю, что я понял — имеет смысл. Я добавил свою реализацию, надеюсь, я не допустил слишком много ошибок: D
Ответ №1:
Я считаю, что это то, что вам нужно
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) amp; 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);
Итак, что здесь происходит?
Мы используем возможность using TypeScript указывать тип this
значения, которое неявно получают функции и методы.
Поскольку мы находимся в static
методе, this
тип ссылается на тип самого класса. Этот тип — это то, что мы можем вызвать new
, то есть сказать, что это конструктор.
Однако мы хотим зафиксировать фактический тип производного класса. С этой целью мы объявляем универсальный тип, T
, который представляет все, что возвращает производный класс, когда мы его вызываем new
. Затем мы заявляем, что this
это конструктор, который создает T
s. Однако при этом мы потеряли доступ к статическим членам базового класса, и мы должны добавить их обратно с пересечением.
Наконец, когда мы вызываем Hotel.findById
, TypeScript выводит T
из typeof Hotel
, потому typeof Hotel
что это тип вызываемого значения findById
.
Примечание: Обычно this
тип for findById
было бы проще написать, т. Е. (new (...args: any[]) => T) amp; typeof BaseModel
Но в этом случае ваш производный класс Hotel
имеет конструктор с несовместимым списком аргументов. Я использовал Pick<typeof BaseModel, keyof typeof BaseModel>
быстрый и грязный способ получения типа, содержащего все члены typeof BaseModel
, кроме сигнатур вызова и построения.
Комментарии:
1. Ну, чувак, это хорошо объяснено и многому научило меня в typescript. Даже если это имеет смысл, я чувствую, что это слишком сложно, по крайней мере, для такого новичка, как я. Итак, я нашел более простой способ добиться аналогичного результата, даже со статическими методами. Посмотрите, если у вас есть шанс.
2. Я рад, что вы нашли это информативным. Я согласен, что более простые подходы предпочтительнее. На самом деле, я бы не рекомендовал ваш оригинальный шаблон, но это возможно, поэтому я хотел объяснить, как это сделать. Я бы также вообще не использовал классы для такого рода вещей, потому что они излишне сложны. Но вы можете, если хотите
3. Я не могу получить доступ к частным или защищенным статическим свойствам, используя это (в статическом методе BaseModel). Как бы мне правильно это сделать?
4. @sergiz это не сработает. Не используйте
private
protected
члены или.
Ответ №2:
Перегрузка статической функции отеля
static async findById (id:string) {
const data = await BaseModel.findById(id)
return new Hotel(data)
}
Я не привык к typescript, поэтому, возможно, кто-то может мне помочь, но после вашего обновления я думаю, вам нужно передать фактическое значение конструктора, а не только тип
Вот пример
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())
Я передаю фактический класс внутри super и использую его внутри getFindId(), чтобы вернуть экземпляр этого класса.
Комментарии:
1. не будет работать, BaseModel.findById возвращает обещание, и вы создаете отель с обещанием; также, вероятно, нежелательно создавать новый отель () для данных.
2. Да, но мне не нравится это решение, посмотрите на мое второе обновление, это совершенно другая структура кода, но мне это нравится больше.
3. Понятно, и я рад, что мы смогли помочь вам найти решение.