Как сделать все свойства класса необязательными в Typescript

Есть способ сделать это:

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> {}

Проблема в том, что я должен использовать классы вместо интерфейсов, потому что мне нужно добавить декораторы для проверки.

Если вы сделаете все свойства необязательными, действительно ли вы расширите класс? Похоже, вы просто хотите создать для меня новый класс...

zenly 04.04.2023 01:22

Соответствует ли такой подход вашим потребностям? Если это так, я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 04.04.2023 02:42

как сказал розовый, вы просто создаете новый класс. возможно, попробуйте извлечь методы, которые вы хотите сохранить, в другой класс и extends этот класс, или определить эти реквизиты необязательными в начале.

Jerryh001 04.04.2023 07:58
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
3
56
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Трудно думать об этом как о преобразовании одного определения класса (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();

также.

Площадка ссылка на код

Другие вопросы по теме