Хм, я не знаю, что такое "MongoModel", и при этом я не знаю, какими должны быть Base
, collectionName
и docId
.Поэтому, если следующее не соответствует вашим ожиданиям и вы не можете его адаптировать, вы можете рассмотреть возможность редактирования своего кода, чтобы он представлял собой минимальный воспроизводимый пример .Но давайте посмотрим, что мы можем сделать здесь.
Во-первых, давайте переименуем ваш Model
в InnerModel
, который будет использоваться для реализации желаемого вами типа, но не будет предоставлен пользователям как есть:
class InnerModel<T> {
constructor(public data: T) {}
doSave(): boolean {
console.log("saving");
return true; // implement me
}
}
Обратите внимание, что я дал ему конструктор для хранения данных типа T
.Теперь цель состоит в том, чтобы создать тип, такой как Model<T>
, который будет действовать как T
и InnerModel<T>
:
type Model<T> = T & InnerModel<T>;
Если у вас есть экземпляр Model<T>
, вы можете получить прямой доступ ко всем свойствамT
, плюс все свойства InnerModel<T>
.Обратите внимание, что я полностью игнорирую то, что происходит, если T
имеет некоторые из тех же имен свойств, что и InnerModel
.Это было бы плохо ... если T
будет {data: number, doSave: boolean}
, у тебя будет плохое время.Так что не делай этого.
В любом случае, цель здесь состоит в том, чтобы создать что-то, что фактически создает экземпляры Model<T>
.
Обратите внимание, что компилятор не может действительно проверить, что то, что вы делаете с этого момента, будетвведите safe, поэтому вам нужно будет использовать утверждения типа или эквивалентный код, чтобы компилятор не выдавал ошибки.Это означает, что вы должны быть осторожны, утверждая только то, что сами можете проверить как истинное.
Сначала мы добавим вспомогательную функцию защиты типа , чтобы помочь определить, является ли имя свойстваизвестный ключ объекта ... мы будем использовать его далее, чтобы помочь компилятору понять, находится ли ключ свойства в самом InnerModel
или во вложенном свойстве data
:
function hasKey<T extends object>(obj: T, k: keyof any): k is keyof T {
return k in obj;
}
Вотосновная часть реализации ... использование прокси для маршрутизации свойства получает / устанавливает либо модель, либо данные, в зависимости от того, где находится ключ
function makeModel<T>(data: T): Model<T> {
return new Proxy(new InnerModel(data), {
get(model: InnerModel<T>, prop: keyof Model<T>) {
return hasKey(model, prop) ? model[prop] : model.data[prop];
},
set(model: InnerModel<T>, prop: keyof Model<T>, value: any) {
return hasKey(model, prop)
? (model[prop] = value)
: (model.data[prop] = value);
}
}) as Model<T>;
}
Есть несколько мест, где этонебезопасно ... обработчики get()
и set()
возвращают any
, а value
набирается как any
в обработчике set()
.Это в основном отключает проверку типов, поэтому мы должны вручную проверить правильность.И компилятор не может видеть, что мы возвращаем Model<T>
в отличие от InnerModel<T>
, поэтому мы должны подтвердить это.
Наконец, мы возьмем эту makeModel()
функцию и будем рассматривать ее какфункция конструктора.В JavaScript вы можете использовать любую функцию в качестве конструктора , и если эта функция возвращает значение, созданным объектом будет это возвращаемое значение.Компилятору действительно не нравится, когда мы делаем это, поэтому нам потребуется двойное утверждение:
const Model = makeModel as unknown as new <T>(data: T) => Model<T>;
Но теперь у нас есть кое-что, что работает:
const n = new Model({
a: "hey",
b: 1,
c: true,
d() {
console.log("D");
}
});
console.log(n.a); // hey
n.a = "you";
console.log(n.a); // you
console.log(n.data.a); // you
n.d(); // D
n.doSave(); // saving
Хорошо, надеюсь, это поможет.Удачи!
Ссылка на код
РЕДАКТИРОВАТЬ: если вы хотите, чтобы он был "гибким", вы должны сделать T
"гибким", выполнив что-то вроде new Model<{[k: string]: any}>({})
, но чем гибче модель, тем менее типизирована.Вам решать, хотите ли вы использовать индексируемый тип или нет, но на самом деле это никак не повлияет на приведенную выше реализацию (или, во всяком случае, не сильно)