Un decorador es una función que asocias a una clase o a un miembro de una clase con @name. Recibe el método original (o la clase, o el campo) más un objeto de contexto que lo describe, y puede devolver un sustituto. TypeScript admite los decoradores estándar sin ninguna opción del compilador.
@logged se ejecuta una sola vez, cuando se define la clase, y sustituye add por el envoltorio que devuelve. Cada llamada pasa por el envoltorio. Los parámetros genéricos mantienen intactos los tipos de this, de los argumentos y del retorno del método, así que add sigue recibiendo dos números y devolviendo un número.
Cómo se compilan los decoradores
Los decoradores estándar vienen de una propuesta de TC39 para JavaScript, y TypeScript los implementa desde TypeScript 5.0. La propuesta todavía no forma parte del estándar de JavaScript, y Node 24 no interpreta la sintaxis @, así que cuando el target es ES2022 (como en estas páginas) el compilador reescribe cada clase decorada en JavaScript normal que llama a funciones auxiliares (__esDecorate y __runInitializers, que se generan al principio del archivo). La salida funciona en cualquier sitio donde funcione ES2022.
Una consecuencia: un archivo con decoradores no se puede ejecutar con el type stripping integrado de Node (node file.ts), que solo quita los tipos y deja el @ en su sitio. Node se detiene con SyntaxError: Invalid or unexpected token. Compílalo antes con tsc o con un bundler.
Tipos de decoradores y sus firmas
Todo decorador estándar tiene la forma (value, context) => replacement | void. Qué es value, y qué puedes devolver, depende de lo que se decora:
| Decora | value | Tipo de contexto | Devuelve |
|---|---|---|---|
| clase | la clase | ClassDecoratorContext | una clase sustituta, o nada |
| método | el método | ClassMethodDecoratorContext | un método sustituto |
| getter / setter | el getter o el setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | un getter o setter sustituto |
| campo | undefined | ClassFieldDecoratorContext | una función que transforma el valor inicial |
campo accessor | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
Todos los objetos de contexto tienen kind, name y addInitializer. El contexto de un miembro de clase también tiene static, private y un objeto access para leer el miembro desde una instancia. Los decoradores también funcionan con miembros estáticos y #private.
Fábricas de decoradores
Para pasar opciones, escribe una función que devuelva un decorador y llámala en el sitio del @. Eso es una fábrica de decoradores:
@retry(3) llama primero a retry, y la función que devuelve es el decorador real. Los decoradores se apilan: en @a @b method(), b se aplica primero y a envuelve el resultado.
Decoradores de clase y addInitializer
Un decorador de clase recibe la propia clase. Puede devolver una subclase para sustituirla, o no devolver nada y simplemente registrarla en algún sitio. context.addInitializer registra código que se ejecuta en un momento concreto: en un decorador de clase, justo después de que la clase esté completamente definida; en un decorador de método, cuando se construye cada instancia.
El decorador de clase va encima de la clase (o después de export). Sin @bound, llamar a loose() lanzaría un error, porque this sería undefined.
Decoradores de campo y de accessor
Un decorador de campo no puede ver ni interceptar asignaciones posteriores: su value es undefined, y lo único que puede devolver es una función que transforma el valor inicial del campo. Para interceptar lecturas y escrituras, declara el campo con la palabra clave accessor, que lo convierte en un par getter y setter respaldado por un almacenamiento privado, y decora eso:
accessor forma parte de la misma propuesta. Genera un getter y un setter reales sobre un campo #private, y por eso p.price = -5 pasa por el set del decorador.
Estándar frente a los experimentalDecorators antiguos
Antes de TypeScript 5.0, los únicos decoradores de TypeScript eran una versión temprana de la propuesta, que se activaba con experimentalDecorators. Esa opción sigue existiendo, y cambia el compilador al modelo antiguo, con otras firmas y otra semántica:
| Estándar (sin opción) | Antiguo (experimentalDecorators) | |
|---|---|---|
| Firma | (value, context) | (target, propertyKey, descriptor) |
| Cómo cambia un método | devuelve una función nueva | modifica descriptor.value |
| Decoradores de parámetros | no admitidos (TS1206) | admitidos |
emitDecoratorMetadata | no admitido | admitido (información de tipos en tiempo de ejecución con reflect-metadata) |
Decoradores accessor ({ get, set, init }), addInitializer | sí | no |
| Basado en | la propuesta de TC39 | un borrador anterior de ella |
Un decorador escrito para un modelo no pasa la comprobación de tipos en el otro. Aquí hay un decorador de estilo antiguo en un proyecto sin la opción:
index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
La solución es reescribirlo con la forma estándar (value, context), como en el primer ejemplo de esta página, o activar el modelo antiguo para todo el proyecto:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS y TypeORM siguen poniendo experimentalDecorators en las configuraciones que generan sus herramientas y que piden sus documentaciones. NestJS y TypeORM también necesitan emitDecoratorMetadata, porque leen tipos en tiempo de ejecución: NestJS para inyectar los parámetros del constructor, TypeORM para asociar propiedades a columnas:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
Si usas uno de esos frameworks, escribe los decoradores a la manera antigua y sigue su documentación. Para código nuevo sin un framework así, usa los decoradores estándar.
Cuándo usar decoradores
Los decoradores encajan con comportamiento transversal que de otro modo se repetiría en muchos métodos: logging, medición de tiempos, caché, reintentos, comprobaciones de acceso, validación y registro de clases en un contenedor o un router. También ocultan el flujo de control, porque quien lee tiene que buscar qué hace @retry antes de saber qué hace el método. Para uno o dos usos, una función de orden superior normal (const fetchData = retry(3, rawFetch)) es más sencilla y además funciona fuera de las clases.
Preguntas frecuentes
¿Qué son los decoradores en TypeScript?
Un decorador es una función que se aplica a una clase o a un miembro de una clase con la sintaxis @name. Recibe lo que se decora y un objeto de contexto, y puede devolver un sustituto: un método envuelto, una clase nueva o una función que transforma el valor inicial de un campo. Los usos habituales son el logging, la validación, la caché y el registro de clases.
¿Necesito experimentalDecorators para usar decoradores en TypeScript?
No. Desde TypeScript 5.0, los decoradores estándar (TC39) funcionan sin ninguna opción. experimentalDecorators cambia el compilador al modelo antiguo de decoradores, sobre el que están construidos frameworks como Angular y NestJS. Los dos usan firmas de función distintas y no son intercambiables.
¿Qué diferencia hay entre los decoradores estándar y experimentalDecorators?
Los decoradores estándar reciben (value, context) y devuelven un sustituto. Los antiguos reciben (target, propertyKey, descriptor) y modifican el descriptor de la propiedad. Solo el modelo antiguo admite decoradores de parámetros y emitDecoratorMetadata; solo el modelo estándar tiene decoradores accessor que devuelven { get, set, init } y context.addInitializer.
¿TypeScript admite decoradores de parámetros?
Solo con experimentalDecorators activado. En el modo estándar, un decorador sobre un parámetro es el error TS1206: Decorators are not valid here, porque la propuesta de TC39 no incluye decoradores de parámetros. Por eso los frameworks de inyección de dependencias que decoran parámetros del constructor necesitan la opción antigua.
¿En qué orden se aplican varios decoradores?
Las expresiones de los decoradores se evalúan de arriba abajo, pero se aplican de abajo arriba: en @a @b method(), b envuelve primero el método y a envuelve el resultado. Así que a es la capa más externa cuando se llama al método.