Есть способ сделать это:
interface CreateDTO {
id: string;
name: string;
price: number;
}
interface UpdateDTO extends Partial<CreateDTO> {}
Но используя классы, что-то вроде этого:
class CreateDTO {
id: string;
name: string;
price: number;
}
class UpdateDTO extends Partial<CreateDTO> {}
Проблема в том, что я должен использовать классы вместо интерфейсов, потому что мне нужно добавить декораторы для проверки.
Соответствует ли такой подход вашим потребностям? Если это так, я напишу ответ с объяснением; если нет, то что мне не хватает?
как сказал розовый, вы просто создаете новый класс. возможно, попробуйте извлечь методы, которые вы хотите сохранить, в другой класс и extends
этот класс, или определить эти реквизиты необязательными в начале.
Трудно думать об этом как о преобразовании одного определения класса (CreateDTO
) в другое определение класса (UpdateDTO
). Во-первых, ваше определение класса-кандидата
class CreateDTO {
id: string; // error!
name: string; // error!
price: number; // error!
}
не может инициализировать необходимые свойства. Если вы включите параметр компилятора --strictPropertyInitialization , который включен в --strict набор функций компилятора (и вы должны это сделать), это приведет к ошибкам компилятора. Исправление этих ошибок заставляет вас думать о том, как на самом деле должен быть создан экземпляр класса, скажем, путем передачи аргументов конструктора для свойств:
class CreateDTO {
id: string;
name: string;
price: number;
constructor(id: string, name: string, price: number) {
this.id = id;
this.name = name;
this.price = price;
}
}
И тогда вы не можете сказать class UpdateDTO extends Partial<CreateDTO>
, потому что Partial
действует только на типы, и вам нужно расширить значение CreateDTO
конструктора класса*. Вы можете написать Partial
функцию, чтобы расширить конструкторы классов универсального типа T
до Partial<T>
:
function Partial<A extends any[], T extends object>(ctor: new (...args: A) => T) {
const ret: new (...args: A) => Partial<T> = ctor;
return ret;
}
и примените его:
class UpdateDTO extends Partial(CreateDTO) { } // okay
Но на самом деле это не решает проблему, потому что вашему новому классу по-прежнему требуются эти аргументы конструктора, поскольку он просто наследуется от CreateDTO
:
const u = new UpdateDTO(); // error!
// -----> ~~~~~~~~~~~~~~~
// Expected 3 arguments, but got 0.
Предположительно, смысл UpdateDTO
в том, что ему не нужны такие аргументы. Вы могли бы продолжать двигаться в этом направлении, но кажется, что вознаграждение не стоит затрат.
Концептуально класс UpdateDTO
на самом деле не является производным от класса CreateDTO
. На самом деле они больше похожи на два отдельных класса, которые каким-то образом происходят от определений вашего интерфейса CreateDTO
и Partial<CreateDTO>
.
Поэтому было бы более плодотворно думать об этом как о преобразовании определений интерфейсов в определения классов. Давайте создадим фабричную функцию, которая принимает определение типа объекта и создает конструктор класса, который принимает аргумент конструктора этого типа и создает экземпляр класса также этого типа. Так:
function DTOClass<T extends object>() {
return class {
constructor(arg: any) {
Object.assign(this, arg)
}
} as (new (arg: T) => T);
}
Реализация использует метод Object.assign() для распространения входного аргумента в создаваемый экземпляр. Затем вы можете сделать с ним свои классы CreateDTO
и UpdateDTO
:
interface CreateDTO {
id: string;
name: string;
price: number;
}
const CreateDTO = DTOClass<CreateDTO>();
const c = new CreateDTO({ id: "abc", name: "def", price: 123 });
c.price = 456;
console.info(c); // { "id": "abc", "name": "def", "price": 456 }
}
interface UpdateDTO extends Partial<CreateDTO> { }
const UpdateDTO = DTOClass<UpdateDTO>();
const u = new UpdateDTO({});
u.name = "ghi";
console.info(u); // { "name": "ghi" }
Выглядит неплохо. Компилятор понимает, что конструкторы классов создают экземпляры соответствующих типов объектов, и работает так, как нужно во время выполнения.
Единственное, что я мог бы изменить здесь, это то, что в случае, когда тип объекта не имеет обязательных свойств, было бы неплохо не требовать, чтобы конструктор класса принимал аргумент. Если я позвоню new UpdateDTO({})
, то new UpdateDTO()
тоже должно работать. Таким образом, мы можем использовать условный тип в DTOClass
, чтобы сделать аргумент необязательным, если {}
будет приемлемым вводом:
function DTOClass<T extends object>() {
return class {
constructor(arg: any) {
Object.assign(this, arg)
}
} as {} extends T ? (new (arg?: T) => T) : (new (arg: T) => T);
}
И теперь вы можете написать
const u = new UpdateDTO();
также.
Если вы сделаете все свойства необязательными, действительно ли вы расширите класс? Похоже, вы просто хотите создать для меня новый класс...