Potentes decoradores de mecanografía: cómo funcionan, en qué se compilan y para qué tareas de aplicación son aplicables

Cada desarrollador Angular vio decoradores en el código de código de tiempo. Se utilizan para describir módulos, configurar la inyección de dependencia o configurar un componente. En otras palabras, los decoradores se usan para describir información adicional, o metadatos, para un marco o compilador (en el caso de Angular). Además, Angular es solo un ejemplo. Hay muchas otras bibliotecas que usan decoradores para simplificar y visualizar el código, como un enfoque declarativo. Como desarrollador de .NET en el pasado, veo muchas similitudes entre los decoradores de TS y los atributos de .NET. Finalmente, el creciente marco de aplicación de back-end NestJS (una abstracción sobre Node) también se basa en el uso intensivo de decoradores y un enfoque declarativo. Cómo funciona todo y cómo usar decoradores en su código,para hacerlo más conveniente y legible? Todos entendemos que después de compilar el código TS, obtenemos el código Javascript. En el que no hay un concepto de decorador, como muchas otras características de Typecript. Por lo tanto, para mí la pregunta más interesante es en qué se convierte el decorador después de la compilación. En este asunto, pronuncié un discurso en la reunión en Minsk y quiero compartir el artículo.






. , .


  • for Module declaration

@NgModule({
  imports: [
    CommonModule,
  ],
  exports: [
  ],
})
export class NbThemeModule {}

  • for component declaration

@Component({
  selector: 'nb-card-header',
  template: `<ng-content></ng-content>`,
})
export class NbCardHeaderComponent {}

, , , .


, tsconfig.json , emitDecoratorMetadata experimentalDecorators, .


{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2017",
  },
}


, — , , , get , . @expression, @ . expression . , , .


, — , , . - .


, — . , -, ( ). , , .



— . Typescript :


declare type MethodDecorator = 
    <T>(
        target: Object, 
        propertyKey: string | symbol, 
        descriptor: TypedPropertyDescriptor<T>) 
=> TypedPropertyDescriptor<T> | void;

, . :


  • ,

:


interface TypedPropertyDescriptor<T> {
    enumerable?: boolean;
    configurable?: boolean;
    writable?: boolean;
    value?: T;
    get?: () => T;
    set?: (value: T) => void;
}

, , .


, - , . Javascript .


, - . — .


class TestServiceDeco {

    @LogTime()
        testLogging() {
        ...
    }
}

, . Typescript , . , - , .

:


function LogTime() {
    return (target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) => {
        const method = descriptor.value;
        descriptor.value = function(...args) {
            console.time(propertyName || 'LogTime');
            const result = method.apply(this, args);
            console.timeEnd(propertyName || 'LogTime');
            return result;
        };
    };
}

, — , . — target, propertyName . .


— , . , , . .


Javascript


"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function LogTime() {
    return (target, propertyName, descriptor) => {
        const method = descriptor.value;
        descriptor.value = function (...args) {
            console.time(propertyName || 'LogTime');
            const result = method.apply(this, args);
            console.timeEnd(propertyName || 'LogTime');
            return result;
        };
    };
}
exports.LogTime = LogTime;

, Typescript . :


Object.defineProperty(exports, "__esModule", { value: true });
const log_time_decorator_1 = require("../src/samples/log-time.decorator");
class TestServiceDeco {
    testLogging() {
...    }
}
__decorate([
    log_time_decorator_1.LogTime(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TestServiceDeco.prototype, "testLogging", null);

__decorate, .


, target prototype , .

, __decorate, , . , . .



, . — . — . — Dependency Injection. . , :


@CustomBehavior({
    singleton: false,
})
class TestServiceDeco {
    constructor() {
        console.log('TestServiceDeco ctor');
    }
}

, Typescript:


declare type ClassDecorator = 
    <TFunction extends Function>(target: TFunction) 
    => TFunction | void;

:


import 'reflect-metadata';

interface Metadata {
    singleton?: boolean;
}

function CustomBehavior(metadata: Metadata) {
    return function(ctor: Function) {
        Reflect.defineMetadata('metadataKey', metadata, ctor);
    }
}

. — singelton- . .


:


  • target
  • reflect-metadata

Reflect-metadata Typescript. , — . , , .


import 'reflect-metadata';

const instancesMap: Map<Object, Object> = new Map<Object, Object>();

function getInstance<T>(tType: new () => T): T {
    let metadata = Reflect.getMetadata('metadataKey', tType) as Metadata;
    if (metadata.singleton) {
        if (!instancesMap.has(tType)) {
            instancesMap.set(tType, new tType());
        }
        return instancesMap.get(tType) as T;
    } else {
        return new tType() as T;
    }
}

  • getInstance, , ,
  • Reflect.getMetadata , . any, as Metadata
  • , . tType: new () => T
  • - , Map

, , .


getInstance, .


, . , Javascript .


. , , , . , Person Age, 18 60. :


class Person {
    @Age(18, 60)
    age: number;
}

:


declare type PropertyDecorator = 
    (target: Object, propertyKey: string | symbol) => void;

:


import 'reflect-metadata';

function Age(from: number, to: number) {
    return function (object: Object, propertyName: string) {
        const metadata = {
            propertyName: propertyName,
            range: { from, to },
        };
        Reflect.defineMetadata(`validationMetadata_${propertyName}`, metadata, object.constructor);
    };
}

, . . , , , . , .


:


class Person {
...
}
__decorate([
    age_decorator_1.Age(18, 60),
    __metadata("design:type", Number)
], Person.prototype, "age", void 0);

, __decorate, .


, — , . — , , .


, :


function validate<T>(object: T) {
    const properties = Object.getOwnPropertyNames(object);
    properties.forEach(propertyName => {
        let metadata = Reflect.getMetadata(metaKey + propertyName, object.constructor);
        if (metadata && metadata.range) {
            const value = object[metadata.propertyName];
            if (value < metadata.range.from || value > metadata.range.to) {
                throw new Error('Validation failed');
            }
        }
    });
}

, , . .


:


const person = new Person();
person.age = 40;
validate(person);
// > validation passed

person.age = 16;
validate(person);
// > validation error


, , , .


, :


declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;


Class-Validator


Class-Validator, . .


export class Post {

    @Length(10, 20)
    title: string;

    @IsInt()
    @Min(0)
    @Max(10)
    rating: number;

    @IsEmail()
    email: string;
}

...

validate(object).then(errors => { // array of validation errors
    if (errors.length > 0) {
        console.log("validation failed. errors: ", errors);
    } else {
        console.log("validation succeed");
    }
});

, . .


, NestJS @UsePipes(new ValidationPipe()) http .



Typescript , . , -. , , , , , , , … Angular NestJS. ( ).


, , , , , !


ps mientras escribía (o más bien traducía su propio artículo), encontré otro allí mismo en el centro, que revela bien el tema de los decoradores. Les doy un enlace


All Articles