Powerful Typescript Decorators - how they work, what they are compiled into, and for which application tasks are applicable

Each Angular developer saw decorators in the timecode code. They are used to describe Modules, configure Dependency Injection, or configure a component. In other words, decorators are used to describe additional information, or metadata, for a framework or compiler (in the case of Angular). Moreover, Angular is just one example. There are many other libraries that use decorators for simplicity and visualization of the code, as a declarative approach. As a .NET developer in the past, I see a lot of similarities between TS decorators and .NET attributes. Finally, the growing NestJS backend application framework (an abstraction over Node) is also built on heavy use of decorators and a declarative approach. How it all works and how to use decorators in your code,to make it more convenient and readable? We all understand that after compiling TS code, we get Javascript code. In which there is no concept of a decorator, like many other Typescript features. Therefore, for me the most interesting question is what the decorator turns into after compilation. Pursuing this issue, I made a speech at the meeting in Minsk and I want to share the article.






. , .


  • 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 while writing (or rather translating his own article), I found another one right there on the hub, which well reveals the theme of decorators. I give a link


All Articles