Decoradores del estándar TC39 en Babel
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Babel 7.1.0 finalmente admite la nueva propuesta de decoradores: puedes probarla usando el plugin @babel/plugin-proposal-decorators 🎉.
Un poco de historia
Los decoradores fueron propuestos por primera vez por Yehuda Katz hace más de tres años. TypeScript lanzó soporte para decoradores en la versión 1.5 (2015) junto con muchas características de ES6. Algunos frameworks importantes, como Angular y MobX, comenzaron a usarlos para mejorar la experiencia del desarrollador: esto popularizó los decoradores y generó en la comunidad una falsa sensación de estabilidad.
Babel implementó inicialmente los decoradores en la versión 5, pero los eliminó en Babel 6 porque la propuesta aún estaba en evolución. Logan Smyth creó un plugin no oficial (babel-plugin-transform-decorators-legacy) que replicaba el comportamiento de Babel 5; posteriormente se trasladó al repositorio oficial de Babel durante la primera versión alfa de Babel 7. Este plugin seguía usando la semántica antigua de decoradores, ya que aún no estaba clara la nueva propuesta.
Desde entonces, Daniel Ehrenberg y Brian Terlson se han unido como coautores de la propuesta junto con Yehuda Katz, y esta ha sido casi completamente reescrita. Aún no se ha decidido todo, y hoy en día no existe una implementación conforme.
Babel 7.0.0 introdujo una nueva opción en el plugin @babel/plugin-proposal-decorators: el parámetro legacy, cuyo único valor válido era true. Este cambio rompedor era necesario para facilitar la transición desde la versión Stage 1 de la propuesta a la actual.
En Babel 7.1.0 estamos implementando soporte para esta nueva propuesta, activado por defecto al usar el plugin @babel/plugin-proposal-decorators. Si no hubiéramos introducido la opción legacy: true en Babel 7.0.0, no sería posible usar la semántica correcta por defecto (equivalente a legacy: false).
La nueva propuesta también admite decoradores en campos y métodos privados. Aún no hemos implementado esta función en Babel (para cada clase, debes elegir entre usar decoradores o elementos privados), pero llegará muy pronto.
¿Qué cambió en la nueva propuesta?
Aunque la nueva propuesta parece muy similar a la anterior, existen varias diferencias importantes que las hacen incompatibles.
Sintaxis
La propuesta anterior permitía cualquier expresión válida del lado izquierdo (literales, expresiones de función y clase, expresiones new y llamadas a funciones, accesos a propiedades simples y computados) como cuerpo de un decorador. Por ejemplo, este código era válido:
class MyClass {
@getDecorators().methods[name]
foo() {}
@decorator
[bar]() {}
}
Esa sintaxis tenía un problema: la notación [...] se usaba tanto para acceso a propiedades dentro del cuerpo del decorador como para definir nombres computados. Para prevenir esta ambigüedad, la nueva propuesta solo permite acceso a propiedades por punto (foo.bar), opcionalmente con argumentos al final (foo.bar()). Si necesitas expresiones más complejas, puedes envolverlas entre paréntesis:
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}
Decoradores de objetos
La versión anterior de la propuesta permitía, además de decoradores para clases y elementos de clase, decoradores para miembros de objetos:
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};
Debido a incompatibilidades con la semántica actual de los objetos literales, se han eliminado de la propuesta. Si los estás usando en tu código, mantente atento porque podrían reintroducirse en una propuesta posterior (tc39/proposal-decorators#119).
Argumentos de las Funciones Decoradoras
El tercer cambio importante introducido por la nueva propuesta concierne a los argumentos que reciben las funciones decoradoras.
En la primera versión de la propuesta, los decoradores de elementos de clase recibían una clase objetivo (u objeto), una clave y un descriptor de propiedad — similar en estructura a lo que pasarías a Object.defineProperty. Los decoradores de clase tomaban como único argumento un constructor objetivo.
La nueva propuesta de decoradores es mucho más potente: los decoradores de elementos toman un objeto que, además de cambiar el descriptor de propiedad, permite modificar la clave, la ubicación (static, prototype u own) y el tipo (field o method) del elemento. También pueden crear propiedades adicionales y definir una función (finisher) que se ejecuta en la clase decorada.
Los decoradores de clase toman un objeto que contiene los descriptores de cada elemento de la clase, permitiendo modificarlos antes de crear la clase.
Actualización
Dadas estas incompatibilidades, no es posible usar decoradores existentes con la nueva propuesta: esto haría la migración muy lenta, ya que las librerías existentes (MobX, Angular, etc.) no pueden actualizarse sin introducir cambios rupturistas.
Para solucionar esto, hemos publicado un paquete de utilidad que envuelve los decoradores en tu código. Después de ejecutarlo,
puedes cambiar tu configuración de Babel para usar la nueva propuesta sin problemas 🎉.
Puedes actualizar tus archivos con una sola línea:
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
Si tu código solo se ejecuta en Node, o si empaquetas tu código con Webpack o Rollup, puedes evitar inyectar la función wrapper en cada archivo usando una dependencia externa:
npm install --save decorators-compat
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write
Para más información, consulta la documentación del paquete.
Preguntas Abiertas
No todo está decidido aún: los decoradores son una característica muy amplia y definirla de la mejor manera es complejo.
¿Dónde Deben Ir los Decoradores en Clases Exportadas?
Esta pregunta ha sido debatida en la propuesta de decoradores: ¿deben ir antes o después de la palabra clave export?
export @decorator class MyClass {}
// or
@decorator
export class MyClass {}
La cuestión subyacente es si la palabra clave export es parte de la declaración de clase o es un "envoltorio". En el primer caso debería ir después de los decoradores, ya que estos van al principio de la declaración; en el segundo debería ir antes, porque los decoradores son parte de la declaración de clase.
¿Cómo Hacer que los Decoradores Interactúen Seguramente con Elementos Privados?
Los decoradores plantean una importante preocupación de seguridad: si es posible decorar elementos privados, entonces los nombres privados (que pueden considerarse como las "claves" de elementos privados) podrían filtrarse. Existen diferentes niveles de seguridad a considerar:
-
Los decoradores no deben filtrar accidentalmente nombres privados. El código malicioso no debería poder "robar" nombres privados de otros decoradores de ninguna manera.
-
Solo los decoradores aplicados directamente a elementos privados podrían considerarse confiables: ¿los decoradores de clase no deberían poder leer ni escribir elementos privados?
-
Hard privacy (uno de los objetivos de la propuesta de campos de clase) significa que los elementos privados solo deberían ser accesibles desde dentro de la clase: ¿debería cualquier decorador tener acceso a nombres privados? ¿Debería ser posible decorar solo elementos públicos?
Estas preguntas requieren más discusión antes de resolverse, y ahí es donde entra Babel.
El papel de Babel
Siguiendo la tendencia del artículo ¿Qué está pasando con la propuesta de pipeline (|>)?, con el lanzamiento de Babel 7 estamos comenzando a utilizar nuestra posición en el ecosistema JS para ayudar aún más a los autores de propuestas, permitiendo a los desarrolladores probar y dar retroalimentación sobre diferentes variaciones de las propuestas.
Por esta razón, junto con la actualización de @babel/plugin-proposal-decorators, hemos introducido una nueva opción: decoratorsBeforeExport, que permite a los usuarios probar tanto export @decorator class C {} como @decorator export default class.
También introduciremos una opción para personalizar la restricción de privacidad de los elementos privados decorados. Estas opciones serán obligatorias hasta que el equipo de TC39 tome una decisión sobre ellas, para que podamos permitir que el comportamiento predeterminado sea lo que especifique la propuesta final.
Si estás usando directamente nuestro parser (@babel/parser, anteriormente babylon) ya puedes usar la opción decoratorsBeforeExport en la versión 7.0.0:
const ast = babylon.parse(code, {
plugins: [
["decorators", { decoratorsBeforeExport: true }]
]
})
Uso
Para su uso en el propio Babel:
- npm
- Yarn
- pnpm
- Bun
npm install @babel/plugin-proposal-decorators --save-dev
yarn add @babel/plugin-proposal-decorators --dev
pnpm add @babel/plugin-proposal-decorators --save-dev
bun add @babel/plugin-proposal-decorators --dev
{
"plugins": ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
}
Consulta la documentación de @babel/plugin-proposal-decorators para ver más opciones.
Tu papel
Como desarrollador de JavaScript, puedes ayudar a delinear el futuro del lenguaje. Puedes probar las diversas semánticas que se están considerando para los decoradores y dar retroalimentación a los autores de la propuesta. ¡Necesitamos saber cómo los estás usando en proyectos reales! También puedes descubrir por qué se tomaron algunas decisiones de diseño leyendo las discusiones en los issues y las actas de reuniones en el repositorio de la propuesta.
Si quieres probar los decoradores ahora mismo, ¡puedes experimentar con las diferentes opciones de presets en nuestro REPL!