@Transform для кастомных преобразований

Декоратор @Transform используется для определения правил преобразования значений при сериализации и десериализации объектов. Он относится к библиотеке class-transformer, но в связке с class-validator часто применяется в DTO-слоях для подготовки данных перед валидацией и дальнейшей обработкой.

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

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


Базовый синтаксис @Transform

Декоратор принимает функцию преобразования и опциональный объект конфигурации:

import { Transform } from 'class-transformer';

class UserDto {
  @Transform(({ value }) => value.trim())
  name: string;
}

Функция получает объект контекста, содержащий:

  • value — исходное значение свойства
  • obj — исходный объект целиком
  • key — имя свойства
  • type — направление трансформации (plainToClass или classToPlain)

Направления трансформации

plainToClass

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

class ProductDto {
  @Transform(({ value }) => Number(value))
  price: number;
}

Вход:

{
  "price": "100"
}

Результат:

ProductDto { price: 100 }

classToPlain

Применяется при сериализации экземпляра класса в обычный объект. Используется для формирования ответа API.

class UserDto {
  @Transform(({ value }) => value.toUpperCase(), { toPlainOnly: true })
  role: string;
}

toClassOnly и toPlainOnly

Конфигурация позволяет ограничить направление применения трансформации:

  • toClassOnly — только при создании класса
  • toPlainOnly — только при сериализации
class ExampleDto {
  @Transform(({ value }) => parseInt(value), { toClassOnly: true })
  id: number;
}

Использование с class-validator

Валидация и трансформация часто используются совместно, где @Transform подготавливает данные, а class-validator проверяет результат.

import { IsNumber } from 'class-validator';
import { Transform } from 'class-transformer';

class OrderDto {
  @Transform(({ value }) => Number(value))
  @IsNumber()
  quantity: number;
}

Сначала выполняется преобразование строки в число, затем проверка типа.


Обработка строковых значений

Наиболее распространённый сценарий — нормализация строковых данных.

Обрезка пробелов

class ProfileDto {
  @Transform(({ value }) => typeof value === 'string' ? value.trim() : value)
  displayName: string;
}

Приведение регистра

class ProfileDto {
  @Transform(({ value }) =>
    typeof value === 'string' ? value.toLowerCase() : value
  )
  email: string;
}

Работа с числовыми значениями

Входящие данные из HTTP-запросов часто приходят в виде строк.

Преобразование в число

class StatsDto {
  @Transform(({ value }) => Number(value))
  views: number;
}

Безопасное преобразование

class StatsDto {
  @Transform(({ value }) => {
    const parsed = Number(value);
    return Number.isNaN(parsed) ? 0 : parsed;
  })
  likes: number;
}

Преобразование булевых значений

HTTP-запросы часто содержат булевы значения в строковом виде.

class FeatureDto {
  @Transform(({ value }) => value === 'true' || value === true)
  isActive: boolean;
}

Расширенный вариант с поддержкой различных форматов:

class FeatureDto {
  @Transform(({ value }) => {
    if (typeof value === 'boolean') return value;
    return value === '1' || value === 'true' || value === 'yes';
  })
  enabled: boolean;
}

Работа с массивами

Разделение строк в массив

class TagsDto {
  @Transform(({ value }) =>
    typeof value === 'string' ? value.split(',') : value
  )
  tags: string[];
}

Вход:

{
  "tags": "nodejs,typescript,backend"
}

Результат:

["nodejs", "typescript", "backend"]

Очистка массива

class TagsDto {
  @Transform(({ value }) =>
    Array.isArray(value)
      ? value.map(v => v.trim()).filter(Boolean)
      : value
  )
  tags: string[];
}

Вложенные объекты

@Transform может применяться для преобразования сложных структур.

class AddressDto {
  @Transform(({ value }) => value?.toUpperCase())
  city: string;
}

class UserDto {
  @Transform(({ value }) => value && new AddressDto(value))
  address: AddressDto;
}

Использование объекта контекста

Контекст трансформации позволяет учитывать метаданные свойства.

class AuditDto {
  @Transform(({ value, key }) => {
    if (key === 'createdAt') {
      return new Date(value);
    }
    return value;
  })
  createdAt: Date;
}

Условные преобразования

Трансформация может зависеть от состояния объекта:

class AccountDto {
  @Transform(({ value, obj }) => {
    if (obj.type === 'admin') {
      return value;
    }
    return null;
  })
  secretKey: string;
}

Комбинирование нескольких трансформаций

Несколько декораторов применяются последовательно:

class SampleDto {
  @Transform(({ value }) => value.trim())
  @Transform(({ value }) => value.toLowerCase())
  username: string;
}

Порядок выполнения влияет на итоговый результат.


Защита от некорректных значений

При обработке внешних данных важна устойчивость к ошибкам.

class SafeDto {
  @Transform(({ value }) => {
    if (typeof value !== 'string') return '';
    return value.trim();
  })
  title: string;
}

Использование с массивами объектов

class ItemDto {
  @Transform(({ value }) => ({
    id: Number(value.id),
    name: String(value.name)
  }))
  item: { id: number; name: string };
}

Типичные ошибки при использовании @Transform

Отсутствие проверки типа

@Transform(({ value }) => value.trim())

Если значение не строка, возникнет ошибка выполнения.

Побочные эффекты

Изменение внешних объектов внутри transform-функции приводит к непредсказуемому поведению:

@Transform(({ obj }) => {
  obj.modified = true;
  return obj.value;
})

Производительность преобразований

Трансформации выполняются при каждом создании экземпляра класса или сериализации. При больших массивах данных избыточная логика внутри @Transform может стать узким местом.

Рекомендуется:

  • избегать тяжёлых вычислений внутри декоратора
  • выносить сложную логику в отдельные функции
  • минимизировать количество вложенных трансформаций

Совместная работа с валидацией и сериализацией

Типичный поток обработки данных:

  1. Получение сырого объекта
  2. Преобразование через @Transform
  3. Валидация через class-validator
  4. Использование в бизнес-логике
  5. Обратная сериализация
class CreateUserDto {
  @Transform(({ value }) => value.trim())
  @IsString()
  name: string;

  @Transform(({ value }) => Number(value))
  @IsNumber()
  age: number;
}

Особенности работы в NestJS

В экосистеме NestJS трансформации активируются через ValidationPipe с параметром transform: true.

app.useGlobalPipes(
  new ValidationPipe({
    transform: true
  })
);

Без включённой трансформации @Transform не применяется.


Приоритеты выполнения

Порядок обработки данных:

  1. Plain object
  2. @Transform
  3. class-transformer внутренние преобразования
  4. class-validator проверки

Любое изменение на уровне @Transform влияет на последующую валидацию.


Расширенные сценарии использования

Нормализация API-ответов

class ResponseDto {
  @Transform(({ value }) => ({
    id: value._id,
    createdAt: value.created_at
  }))
  data: any;
}

Маскирование данных

class SecretDto {
  @Transform(({ value }) => value.replace(/.(?=.{4})/g, '*'))
  cardNumber: string;
}

Вычисляемые поля

class UserDto {
  @Transform(({ obj }) => `${obj.firstName} ${obj.lastName}`)
  fullName: string;
}

Ограничения механизма

  • Не выполняет асинхронные операции
  • Не предназначен для сетевых запросов
  • Не заменяет бизнес-логику
  • Не управляет состоянием приложения

Механизм строго синхронный и ориентирован на локальные преобразования данных.


Архитектурная роль в DTO-слое

@Transform формирует промежуточный слой между внешними данными и внутренними структурами. Он уменьшает необходимость ручного преобразования в сервисах и контроллерах, централизуя правила нормализации на уровне моделей данных.