Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров2.9K

Всем привет! Наткнулся недавно на статью Переосмысление 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.: Это моя первая статья на Хабре, буду рад любой конструктивной критике :-)

Теги:
Хабы:
+5
Комментарии16

Публикации

Работа

Ближайшие события