Декораторы TypeScript: поддержка и ограничения

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

В TypeScript существует два основных режима работы с декораторами:

  • legacy-декораторы (классическая реализация TypeScript до стандартизации TC39)
  • stage 3 decorators (современный стандарт ECMAScript)

Эти два подхода несовместимы по поведению, что критично для сборщиков и транспайлеров.


Legacy-декораторы: особенности реализации TypeScript

Legacy-декораторы активируются при включении следующих опций в tsconfig.json:

  • experimentalDecorators: true
  • emitDecoratorMetadata: true (опционально)

Основные особенности:

1. Порядок вызова

Декораторы применяются снизу вверх, справа налево, что влияет на композицию модификаторов.

2. Изменяемость сущностей

Декоратор может заменить:

  • конструктор класса
  • метод
  • дескриптор свойства

3. Интеграция с reflect-metadata

При использовании emitDecoratorMetadata TypeScript генерирует метаданные типов, доступные через reflect-metadata.

Пример поведения:

  • типы параметров сохраняются как runtime-метаданные
  • используется ключевая система design:type, design:paramtypes

4. Рантайм-зависимость

Legacy-декораторы требуют выполнения дополнительного JavaScript-кода после транспиляции.


Stage 3 decorators: стандартизация ECMAScript

Современный стандарт декораторов (Stage 3) изменяет модель работы:

  • декораторы становятся чистыми функциями
  • изменяется модель метаданных
  • запрещены некоторые паттерны мутации

Ключевые изменения:

1. Новый сигнатурный формат

Декоратор теперь получает контекст:

  • kind (class, method, field)
  • name
  • access
  • metadata

2. Ограничение мутаций

Больше нельзя произвольно перезаписывать дескрипторы методов.

3. Лучшая предсказуемость

Поведение декораторов становится ближе к функциональному программированию.


Различия между legacy и stage 3

Характеристика Legacy Stage 3
Статус устаревающий стандарт ECMAScript
Изменение метода возможно ограничено
Метаданные reflect-metadata встроенные
Совместимость широкая ограниченная
Использование в фреймворках массовое постепенно внедряется

Поддержка декораторов в esbuild

esbuild ориентирован на сверхбыструю трансформацию кода, но его подход к декораторам ограничен архитектурно.

1. Отсутствие полноценного type-aware трансформа

esbuild не является TypeScript-компилятором. Он:

  • удаляет типы
  • транспилирует синтаксис
  • не выполняет глубокий анализ TS-типов

Из-за этого:

  • emitDecoratorMetadata не поддерживается
  • метаданные типов не генерируются

2. Поддержка legacy-декораторов

esbuild поддерживает базовую трансформацию legacy-декораторов при включении:

  • --target=esnext
  • --loader=ts

Однако поведение имеет ограничения:

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

3. Stage 3 decorators

Поддержка stage 3 декораторов в esbuild:

  • частичная или экспериментальная (в зависимости от версии)
  • требует дополнительных флагов или плагинов
  • не всегда соответствует спецификации TypeScript

Ограничения архитектуры esbuild

1. Отсутствие генерации runtime-метаданных

Ключевая проблема:

  • TypeScript компилятор использует типовую информацию
  • esbuild не хранит TS-типы после парсинга

Следствие:

  • невозможна генерация design:type
  • невозможен полноценный DI в стиле Angular без дополнительного шага

2. Отсутствие интеграции с reflect-metadata

Библиотека reflect-metadata требует:

  • точной генерации метаданных
  • строгого соответствия TS-компилятору

esbuild этого не обеспечивает.


3. Ограниченная трансформация AST

esbuild использует собственный оптимизированный парсер:

  • без полной TypeScript semantic model
  • без type checker pipeline
  • без emit hooks уровня TypeScript compiler API

Практические последствия для разработки

1. Использование фреймворков

Фреймворки, завязанные на декораторы:

  • NestJS
  • Angular (частично)
  • TypeORM (legacy-режим)

требуют осторожной интеграции с esbuild.

Часто возникает необходимость:

  • предварительной компиляции через TypeScript
  • или использования Babel/SWC вместо esbuild для TS слоя

2. Пайплайн сборки

Типовая схема при использовании декораторов:

TypeScript (tsc) → esbuild (bundle/minify)

или

Babel (decorators transform) → esbuild

3. Потеря runtime-метаданных

Без дополнительного шага:

  • dependency injection не работает корректно
  • автоматическая валидация DTO затруднена
  • ORM-маппинг может ломаться

Совместимость с Babel и альтернативами

В экосистеме декораторов часто применяются альтернативы:

Babel

  • поддерживает stage 3 decorators через @babel/plugin-proposal-decorators
  • умеет работать с legacy режимом
  • может генерировать метаданные при корректной конфигурации

SWC

  • более близок к esbuild по скорости
  • поддержка decorators развивается активно
  • ограниченная совместимость с legacy-экосистемой

Конфигурационные нюансы TypeScript

Ключевые параметры, влияющие на поведение декораторов:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

При использовании esbuild:

  • эти параметры не имеют полного эффекта
  • часть логики игнорируется на уровне трансформации

Типовые ошибки при использовании esbuild с декораторами

1. Потеря типов параметров

Метаданные становятся undefined или отсутствуют полностью.

2. Несовпадение порядка декораторов

В сложных композициях возможны расхождения с поведением tsc.

3. Неработающие DI-контейнеры

Системы инверсии зависимостей перестают корректно резолвить типы.

4. Ошибки при runtime

Проблемы проявляются только в рантайме:

  • undefined metadata
  • некорректные инстансы классов
  • ошибки рефлексии

Роль декораторов в современной сборке

Декораторы постепенно переходят из экспериментальной зоны в стандарт ECMAScript, однако экосистема инструментов развивается асинхронно:

  • TypeScript поддерживает оба стандарта
  • esbuild фокусируется на скорости, а не на семантике
  • полноценная поддержка требует внешних трансформаций

Это формирует устойчивую архитектурную модель:

  • компилятор для семантики
  • bundler для производительности