Композиция DTO в TypeScript

от автора

Всем привет! Наткнулся недавно на статью Переосмысление DTO в Java и решил интерпретировать предложенный подход к TypeScript и NestJS. Данный стек используется нашей командой повсеместно, отсюда и мой выбор.

Вспомним, что такое DTO в рамках фреймворка NestJS. Этот инструмент помогает описывать структуры данных передаваемых между клиентом и сервером, а также слоями приложения. Для построения такого рода объектов чаще всего используется класс с описанными полями и валидацией. Вроде ничего сложного.

Проблемы начинаются в тот момент, когда для некой сущности в приложении появляется с десяток DTO’шек, что приводит нас к дублированию кода. Одни и те же поля вроде name, email, id повторяются из DTO в DTO, а внесение изменений превращается в рутинную правку десятков файлов.

В этой статье я попробую показать, как можно организовать DTO более модульно. Такой подход позволяет писать более читаемый, масштабируемый код с соблюдением DRY принципа. Начнем!

Базовая структура

Для начала рассмотрим привычный способ создания DTO. Возьмем для примера создание юзера:

export class CreateUserDto {   name: string;   email: string;   password: string; }

После добавления валидации наш класс будет выглядеть следующим образом:

import { IsString, IsEmail, Length } from 'class-validator';  export class CreateUserDto {   @IsString()   @Length(2, 50)   name: string;    @IsEmail()   email: string;    @IsString()   @Length(6, 100)   password: string; }

Это читаемо и удобно, но до тех пор, пока не появляется UpdateUserDto, UserResponseDto, и другие варианты. Понятно, что в итоге мы получим дубликаты полей с одними и теми же правилами. И когда бизнес попросит нас что-то поменять, нам придется идти по всем этим файлам.

Возникает закономерный вопрос: что мы можем с этим сделать?

Путь к модульности через миксины

Обычные интерфейсы и implements в TypeScript не подходят для аннотаций валидации, таких как @IsEmail или @Length, потому что интерфейсы не существуют в рантайме. А class-validator использует именно рантайм-декораторы.

Дабы сохранить модульность и обеспечить рабочую валидацию, можно использовать функции-миксины. Это функции, которые принимают базовый класс и возвращают новый — с добавленным полем и декораторами.

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

// dto/fields/with-name.dto.ts import { IsString, Length } from 'class-validator';  export function WithName<T extends new (...args: any[]) => any>(Base: T) {     abstract class WithName extends Base {         @IsString()         @Length(2, 50)         name: string;     }     return WithName; }  // dto/fields/with-email.dto.ts import { IsEmail } from 'class-validator';  export function WithEmail<T extends new (...args: any[]) => any>(Base: T) {     abstract class WithEmail extends Base {         @IsEmail()         email: string;     }     return WithEmail; }  // dto/fields/with-password.dto.ts import { IsString, Length } from 'class-validator';  export function WithPassword<T extends new (...args: any[]) => any>(Base: T) {     abstract class WithPassword extends Base {         @IsString()         @Length(6, 100)         password: string;     }     return WithPassword; }

Теперь чтобы описать тот же DTO для создания пользователя достаточно собрать его из строительных блоков:

// dto/create-user.dto.ts import { WithName } from './fields/with-name.dto'; import { WithEmail } from './fields/with-email.dto'; import { WithPassword } from './fields/with-password.dto';  export class CreateUserDto extends WithName(WithEmail(WithPassword(class {}))) {}

Все поля будут включены в финальный класс с валидацией. NestJS и class-validator увидят все декораторы, как будто они определены напрямую.

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

Заключение

Использование модульных DTO в TypeScript — это хороший способ улучшить структуру и поддерживаемость кода, особенно в больших проектах, где DTO могут повторяться во многих местах. Он позволит снизить вероятность ошибок при изменении, а также упростит процесс сопровождения.

А как вы организуете DTO в своих проектах? Используете ли подобный подход или предпочитаете что-то другое? Какие еще минусы видите в таком подходе?

P.S.: Это моя первая статья на Хабре, буду рад любой конструктивной критике 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/909160/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *