Decorators en TypeScript: clases, métodos y metadata | Nicolás Garzón
decorator
TypeScript
Copiar @ sealed
class ProductService { } Texto
Copiar Decorators estándar modernos
→ modelo compatible con la propuesta de JavaScript
Decorators legacy
→ activados mediante experimentalDecoratorsSus firmas, orden, capacidades y metadata no son intercambiables.
TypeScript
Copiar function loggedMethod <
TThis,
TArgs extends unknown [ ] ,
TResult,
> (
originalMethod : (
this : TThis,
... args: TArgs
) => TResult,
context: ClassMethodDecoratorContext<
TThis,
(
this : TThis,
... args: TArgs
) => TResult
> ,
) {
const methodName = String ( context. name) ;
return function (
this : TThis,
... args: TArgs
) : TResult {
console . log ( ` Calling ${ methodName} ` , args) ;
return originalMethod . apply ( this , args) ;
} ;
} TypeScript
Copiar class ProductService {
@ loggedMethod
save ( product: Product) : Product {
return product;
}
}
El valor decorado.
Un objeto context con información y capacidades.
Según el tipo de miembro, el contexto puede incluir:
TypeScript
Copiar context. kind;
context. name;
context. static;
context. private;
context. addInitializer;
context. access; No todos los contexts exponen exactamente las mismas operaciones.
TypeScript
Copiar if ( context. kind !== "method" ) {
throw new TypeError ( "Method required" ) ;
} Valores posibles incluyen:
"class"
"method"
"getter"
"setter"
"field"
"accessor"
TypeScript
Copiar function registered<
TClass extends abstract new ( ... args: any [ ] ) => any ,
> (
Constructor: TClass,
context: ClassDecoratorContext< TClass> ,
) : TClass | void {
registry. set ( String ( context. name) , Constructor) ;
} Puede observar o retornar otro constructor compatible.
TypeScript
Copiar function withTimestamp<
TClass extends new ( ... args: any [ ] ) => object,
> ( Constructor: TClass) {
return class extends Constructor {
readonly createdAt = new Date ( ) ;
} ;
} La nueva clase debe conservar una relación razonable con el constructor original. El tipo inferido en el sitio decorado no siempre expone miembros añadidos por un decorator; para APIs públicas puede ser mejor una factory o mixin explícito.
Un wrapper debe preservar el receptor:
TypeScript
Copiar return function ( this : TThis, ... args: TArgs) {
return originalMethod . apply ( this , args) ;
} ; Una arrow capturaría el this del decorator y cambiaría el comportamiento.
Un field decorator moderno no recibe el valor inicial directamente. Puede retornar una función initializer:
TypeScript
Copiar function trimmed (
_value: undefined ,
context: ClassFieldDecoratorContext< unknown , string > ,
) {
if ( context. private) {
throw new Error ( "Public field required" ) ;
}
return function ( initialValue: string ) : string {
return initialValue. trim ( ) ;
} ;
} TypeScript
Copiar class Product {
@ trimmed
name = " Keyboard " ;
} TypeScript
Copiar class Account {
@ validatedPercentage
accessor discount = 0 ;
} Un accessor decorator puede reemplazar get, set e init, trabajando sobre almacenamiento oculto creado por el auto-accessor.
TypeScript
Copiar function validatedPercentage (
value: ClassAccessorDecoratorTarget< unknown , number > ,
_context: ClassAccessorDecoratorContext< unknown , number > ,
) {
return {
get: value. get,
set ( this : unknown , nextValue: number ) {
if ( nextValue < 0 || nextValue > 100 ) {
throw new RangeError ( "Invalid percentage" ) ;
}
value. set . call ( this , nextValue) ;
} ,
init ( initialValue: number ) {
return Math. min ( 100 , Math. max ( 0 , initialValue) ) ;
} ,
} ;
} Permite registrar trabajo que se ejecutará durante inicialización.
TypeScript
Copiar function bound (
originalMethod: Function ,
context: ClassMethodDecoratorContext,
) {
if ( context. private) {
throw new Error ( "Private methods are not supported" ) ;
}
context. addInitializer ( function ( ) {
const name = context. name as keyof this ;
this [ name] = originalMethod . bind ( this ) as this [ keyof this ] ;
} ) ;
} Una implementación real debe tipar cuidadosamente el acceso; la idea es ligar el método por instancia.
TypeScript
Copiar function minimumLength ( length: number ) {
return function (
_value: undefined ,
context: ClassFieldDecoratorContext< unknown , string > ,
) {
return function ( initialValue: string ) : string {
if ( initialValue. length < length) {
throw new RangeError (
` ${ String ( context. name) } is too short ` ,
) ;
}
return initialValue;
} ;
} ;
} La factory recibe configuración primero y devuelve el decorator.
TypeScript
Copiar @ first ( )
@ second ( )
class Example { } Las expresiones de decorator se evalúan en orden de aparición; su aplicación y initializers siguen reglas específicas del modelo y del tipo de elemento. No dependas de efectos sutiles entre decorators sin pruebas y documentación.
TypeScript
Copiar import type { controller } from "./decorators.js" ; No es válido si controller se usa como decorator, porque debe existir al ejecutar.
TypeScript
Copiar import { controller } from "./decorators.js" ; Los decorators estándar no generan automáticamente metadata de tipos de parámetros.
TypeScript
Copiar class Service {
constructor ( repository: ProductRepository) { }
} Interfaces y generics se borran; no existe una reflexión automática completa sobre esos tipos.
Algunos ecosistemas construyen metadata propia mediante decorators, initializers, symbols o transformaciones adicionales.
JSON
Copiar {
"compilerOptions" : {
"experimentalDecorators" : true
}
} Una firma legacy de método recibe:
TypeScript
Copiar function legacyDecorator (
target: object,
propertyKey: string | symbol ,
descriptor: PropertyDescriptor,
) : void { } Esto no coincide con (value, context) del modelo estándar.
JSON
Copiar {
"compilerOptions" : {
"experimentalDecorators" : true ,
"emitDecoratorMetadata" : true
}
} Puede emitir llamadas auxiliares con metadata de diseño basada en tipos runtime disponibles.
Interfaces desaparecen.
Generics pierden sus argumentos.
Unions y tipos complejos se reducen.
Requiere una librería de reflexión en muchos frameworks.
Añade acoplamiento a la emisión legacy.
No representa el sistema de tipos completo.
TypeScript
Copiar class Controller {
method ( @ Inject ( "repository" ) repository: Repository) { }
} Los parameter decorators pertenecen al modelo legacy. Los decorators estándar modernos no soportan parámetros de esta forma.
Frameworks como versiones o configuraciones concretas de NestJS y Angular pueden depender de decorators legacy y metadata emitida.
Sigue la configuración y versión del framework. No mezcles ejemplos modernos con firmas legacy dentro del mismo proyecto.
TypeScript
Copiar @ validate
class ProductInput { } La validación solo existe si el decorator genera o registra lógica runtime real. El símbolo @ no convierte automáticamente una clase en schema.
TypeScript
Copiar @ transactional
save ( ) { } TypeScript
Copiar withTransaction ( ( ) => save ( ) ) ; El decorator reduce repetición, pero oculta cuándo y cómo se aplica comportamiento. Para lógica de dominio central, una composición explícita puede ser más fácil de seguir y probar.
Versión de TypeScript.
Decorators estándar o legacy.
Target.
Bundler/transpiler.
Configuración del framework.
Comprueba que editor, tsc, test runner y build utilicen el mismo modelo.
TypeScript
Copiar function deprecated (
originalMethod: Function ,
context: ClassMethodDecoratorContext,
) {
return function ( this : unknown , ... args: unknown [ ] ) {
console . warn (
` ${ String ( context. name) } is deprecated ` ,
) ;
return originalMethod . apply ( this , args) ;
} ;
}
Decir que un decorator se ejecuta durante transpilation y no runtime.
Mezclar firmas estándar y legacy.
Activar experimentalDecorators para un ejemplo moderno sin necesidad.
Esperar parameter decorators en el modelo estándar.
Creer que emitDecoratorMetadata conserva interfaces y generics.
Importar un decorator con import type.
Retornar un wrapper que pierde this, parámetros o retorno.
Añadir miembros y esperar que el tipo de la clase los conozca automáticamente.
Depender de orden y efectos entre decorators sin documentarlos.
Usar decorators para ocultar reglas importantes del dominio.
Decorators son funciones runtime aplicadas durante definición de clases.
El modelo moderno recibe valor y context.
Puede reemplazar clases o miembros y registrar initializers.
Fields y accessors tienen contratos diferentes.
Decorator factories reciben configuración.
El sistema legacy usa firmas y opciones distintas.
Parameter decorators y emitDecoratorMetadata pertenecen al modelo legacy.
Los tipos borrados no pueden recuperarse completamente por reflexión.
Tooling y framework deben coincidir en el mismo sistema.
¿Por qué una interface de constructor no aparece completa en emitDecoratorMetadata?
Respuesta Porque las interfaces existen únicamente para el checker y se eliminan durante la emisión; la metadata legacy solo puede referirse a valores runtime disponibles y representaciones generales.
Testing de tipos y @ts-expect-error explica cómo proteger una API estática además de sus pruebas runtime.