
18 апреля 2022 года, после 5 лет доработки (первый коммит от 30 апреля 2017 года), proposal по декораторам наконец-то достиг 3 стадии, что означает что у него есть спецификация, тестовая имплементация и осталась только полировка на основе фидбека от разработчиков. Учитывая что это уже четвертая (!) итерация декораторов, их переход в стадию принятия это эпохальное событие для JS - не припомню ни одной другой фичи, которая прошла такой длинный и тернистый путь, с диаметрально разными подходами и аж двумя разными legacy-имплементациями, в Babel и TypeScript. Давайте же посмотрим на неё повнимательней.
Ссылки
Репозиторий самого предложения, включая все предыдущие версии (в истории коммитов).
История предложений, включая ссылки на все четыре основные версии.
Кстати, новая версия датируется в Babel как 2021-12 - потому что была представлена на саммите TC39 в декабре 2021 года.
Чем отличается от предыдущих версий
Во-первых, новые декораторы пока работают только с классами и их элементами. Впрочем, предложения по расширению той же логики на функции/параметры/объекты/переменные/аннотации/блоки/инициализаторы есть, но в текущую спеку не входят (что неудивительно, вряд ли кто-то хочет потратить еще 5 лет на достижение Stage 4).
Во-вторых, главное отличие новых декораторов: они работают только с сущностью которую декорируют (класс, поле класса, метод, геттер/сеттер и аксессор - новая сущность, о которой далее), а не с дескрипторами свойств и/или прототипами классов, как легаси подходы.
То есть они не способны добавить новые сущности в прототип/инстанс класса или хотя бы изменить их вид (с поля на геттер/сеттер, например), а могут только преобразовать ту сущность, которая описана в исходном коде - обернуть её в дополнительную логику или полностью заменить на другую, но того же вида.
Это было сделано в первую очередь под давлением разработчиков V8 основных движков, так как чрезмерная гибкость предыдущих декораторов крайне плохо подходила для оптимизации кода в рантайме - именно поэтому принятие декораторов так затянулось.
Демо и синтаксис применения декораторов
Ну и сразу полный пример со всеми возможными комбинациями синтаксиса:
//export должен быть перед декоратором export default //декоратор класса, может изменять сам класс @defineElement("some-element") class SomeElement extends HTMLElement { //декоратор поля - может заменить значение поля при инициализации класса //все дальнейшие чтения/записи он не отслеживает @inject('some-dep') dep //новый синтаксис - аксессор //по факту просто сахар для пары геттер/сеттер //похож на автоматически реализуемые свойства в C# //могут быть и приватными и статическими //декоратор может отслеживать чтение/запись @reactive accessor clicked = false //ну с методами и прочим все как обычно @logged someMethod() { return 42 } //да, с приватными элементами тоже работает, как и со статическими //название декоратора может быть через точку @random.int(0, 42) #val @logged get val() { return this.#val } @logged set val(value) { this.#val = value } //апофеоз: //статический приватный аксессор c декоратором со сложным доступом @(someArr[3].someFunc('param')) static acсessor #name = 'some-element' }
Текущие имплементации полифиллов этот пример полностью переварить еще не могут но, думаю, в скором времени это исправится.
Синтаксис для применения декораторов в целом не слишком отличается от привычного, есть только пара деталей:
Декоратор класса должен идти после export (если он есть) - это наверное главное отличие от статус-кво.
Для "обычного" применения декоратора можно использовать идентификатор, точку и вызов функции -
@dotted.form.with('some-call')Для "сложного" применения можно использовать синтаксис со скобками:
@(complex[1])
Написание декораторов
Тут никаких особых сюрпризов - декоратор это обычная функция с таким типом:

context предоставляет, как ни странно, контекст, сведения о месте применения декоратора, где:
kind- вид элемента, на который применяется декоратор;name- название элемента;access- объект который позволяет в произвольный момент времени получить/установить значение элемента, может пригодиться, например, для DI. Разрешен только для элементов класса, но не для самих классов (то естьgetилиsetесть только когдаkind != 'class');privateиstatic- есть ли у элемента класса соответствующие модификаторы;addInitializerпозволяет выполнить код после того как сам класс (не инстанс!) или элемент класса полностью определен - например, в нем можно зарегистрировать класс в DI или забиндить метод. Не применим только для поля класса (то есть определен когдаkind != 'field'- об этом далее).
Input и Output зависят от kind, но в целом Input - это значение элемента как оно написано в коде, а Output - значение на которое оно будет заменено в рантайме.
Важный нюанс - для полей класса (когда kind == 'field') Input всегда undefined, а Output может быть функцией вида (initValue: unknown) => any - эта функция вызывается при инициализации класса для вычисления начального значения поля. Именно из-за этого для поля класса не передается addInitializer - Output его заменяет.
Пример декоратора logged:
function logged(value, { kind, name }) { if (kind === "method") { return function (...args) { console.log(`starting ${name} with arguments ${args.join(", ")}`); const ret = value.call(this, ...args); console.log(`ending ${name}`); return ret; }; } if (kind === "field") { return function (initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; }; } if (kind === "class") { return class extends value { constructor(...args) { super(...args); console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`); } } }
Ну или вот customElement с использованием addInitializer:
function customElement(name) { return (value, { addInitializer }) => { addInitializer(function() { customElements.define(name, this); }); } } @customElement('my-element') class MyElement extends HTMLElement { static get observedAttributes() { return ['some', 'attrs']; } }
Больше примеров (в том числе и с применением access для DI) смотрите на гитхабе.
Аксессоры
Ограничения новых декораторов в виде запрета на изменение вида элемента в целом логичны, но они убивают один крайне важный юзкейс декораторов - когда поле превращается в пару геттер/сеттер с дополнительной логикой вокруг. Это может быть, например, логгирование изменений поля для отладки, а может быть полноценная система реактивности, как в MobX, который, по сути, основан на этом хаке:
import {computed, observable, autorun} from 'mobx' class Counter { //вот здесь поле превращается в геттер/сеттер @observable num = 1 //а будет так @observable accessor num = 1 @computed get double() { return this.num * 2 } } const counter = new Counter() //выведет 2 autorun(() => console.log(counter.double)) //когда изменяем num, изменится и double counter.num = 2 //autorun выполняется снова и выводит 4
С новыми декораторами все такие поля придется помечать как accessor что, конечно, не слишком весело, но в целом терпимо и может отслеживаться, например, тайпскриптом. Под капотом работать это будет примерно так:
class C { accessor x = 1; } //Раскрывается в... class C { #x = 1; get x() { return this.#x; } set x(val) { this.#x = val; } }
Имплементации
Пока ждем реализации в основных тулзах - в первую очередь это, конечно, поддержка аксессоров как нового синтаксиса. Когда IDE, TypeScript и Babel (esbuild и т.д.) смогут их корректно обрабатывать, сделать полифиллы будет не так и сложно.
И я крайне надеюсь что TypeScript будет корректно обрабатывать типы декораторов при замене значений - сейчас декоратор никак не может повлиять на тип декорируемого значения.
Ссылки для отслеживания внедрения:
TypeScript - фича включена в планы на версию 4.8.
esbuild - ждут реализации в TS/node/браузерах.
Ну а потом последует волна переезда на новую реализацию со стороны экосистемы. К счастью, декораторы в JS не так и распространены, и при этом новые декораторы могут быть реализованы в библиотеках вместе со старыми - их сигнатура отличается от Babel/TS декораторов.
Дождались, в общем.
