Mighty Decorators in Typescript

what can we get out of it

@ValentinKononov

@ValentinKononov

Discussion

@ValentinKononov

What is TS Decorator

@ValentinKononov

Discussion

@ValentinKononov

What is Typescript Decorator

?

What is TS Decorator

@ValentinKononov

"A Decorator is a special kind of declaration that can be attached to a class declaration,

method, accessor, property, or parameter"

 

typescriptlang.org/docs/handbook/decorators.html

Decorator === Function

Decorator Samples

@NgModule({
  imports: [
    CommonModule,
  ],
  exports: [
  ],
})
export class NbThemeModule {}
@Component({
  selector: 'nb-card-header',
  template: `<ng-content></ng-content>`,
})
export class NbCardHeaderComponent {}
@Entity()
export class Project {
    @PrimaryGeneratedColumn()
    id: number;

    @Column('text')
    name: string;
}

@ValentinKononov

Experimental Feature

> tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2017",
  },
}

@ValentinKononov

Discussion

@ValentinKononov

How we can use it

?

Practical Use Cases

What's the purpose?

  • custom behavior
    • logging
    • validation
  • metadata
    • like in Angular
  • meta-programming
  • declarative programming
  • code readability

Real Use Cases

@ValentinKononov

Discussion

class TestServiceDeco {

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

?

@ValentinKononov

Function Decorator

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;
        };
    };
}

@ValentinKononov

Function Decorator

  • target - object where function call happened
  • propertyName - name of function or property of the target
  • descriptor - has function as reference

@ValentinKononov

Compiled 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;

@ValentinKononov

decorator itself

Compiled Javascript

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);

@ValentinKononov

decorators call

Compiled Javascript

@ValentinKononov

Javascript uses

 

__decorate

 

function to call your decorators

Compiled Javascript

class TestServiceDeco {
  @LogResult()
  @LogTime()
  public testLogging() {...}
}

@ValentinKononov

several decorators

__decorate([
    log_time_decorator_1.LogResult(),
    log_time_decorator_1.LogTime(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TestServiceDeco.prototype, "testLogging", null);

Class Decorator

@CustomBehavior({
    singleton: false,
    tags: ['singleton', 'test', 'service'],
})
class TestServiceDeco {
    constructor() {
        console.log('TestServiceDeco ctor');
    }
}

@ValentinKononov

personal dependency injector

Class Decorator

import 'reflect-metadata';

interface Metadata {
    singleton?: boolean;
    tags?: string[];
}

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

@ValentinKononov

decorator itself

Class Decorator

  • ctor - reference to class constructor

@ValentinKononov

Compiled Javascript

TestServiceDeco = __decorate([
    class_meta_decorator_1.CustomBehavior({
        singleton: false,
        tags: ['singleton', 'test', 'service'],
    }),
    __metadata("design:paramtypes", [])
], TestServiceDeco);

@ValentinKononov

Schema

@ValentinKononov

Class Decorator

import 'reflect-metadata';

const diMap: 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 (!diMap.has(tType)) {
            diMap.set(tType, new tType());
        }
        return diMap.get(tType) as T;
    } else {
        return new tType() as T;
    }
}

usage

const instance1 = getInstance(TestServiceDeco);

Property Decorator

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

@ValentinKononov

decorator itself

import 'reflect-metadata';

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

Property Decorator

  • prototype - reference to object prototype
  • propertyName - name of function or property of the target

@ValentinKononov

@ValentinKononov

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

Compiled Javascript

Property Decorator

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

usage

const person = new Person();
person.age = 40;
validate(person);

Existing Options

@ValentinKononov

Class Validator

export class Post {

    @Length(10, 20)
    title: string;

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

    @IsEmail()
    email: string;
}
npm install class-validator --save
const post = new Post();
post.title = 'Test';

validate(object)
  .then(errors => {
    ...
});

@ValentinKononov

ts-stronger-types

@Typed()
    public multiplyChecked(num: number, num2: number): number {
        return  num * num2;
    }
npm install ts-stronger-types --save

@ValentinKononov

Conclusion

@ValentinKononov

  • Custom behavior
  • Meta-programming
  • Readable code
  • Enjoy coding!

Mighty Decorators in Typescript - what could we get out of it

By Valentin Kononov

Mighty Decorators in Typescript - what could we get out of it

Initial version of slides for topic

  • 627