Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confusion about when decorators are called in TypeScript

I have been under the impression that decorators in TypeScript are called after the constructor of a class. However, I was told otherwise, for instance, the top answer of this post claims that Decorators are called when the class is declared—not when an object is instantiated. A Udemy instructor of an Angular course I was enrolled in also told me that decorators in Typescript run before property initialization.

However, my experiments on this subject seem to indicate otherwise. For instance, this is a simple Angular code with property binding:

test.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-test',
  template: '{{testString}}'  
})
export class TestComponent{    
  @Input() testString:string ="default string";    
  constructor() {
    console.log(this.testString);
   }       
}

app.component.html

<app-test testString="altered string"></app-test>

When I execute the code, the console logs "default string" instead of "altered string". This proves that decorators are called after the constructor of a class executes.

Can somebody give me a definite answer of when decorators are called? Because my research online are contradicting the experiments I make. Thank you!

like image 334
Eddie Lin Avatar asked Mar 24 '18 06:03

Eddie Lin


People also ask

Why would you use a decorator in TypeScript?

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. NOTE Decorators are an experimental feature that may change in future releases.

Where can TypeScript decorators be applied to?

A Decorator is a special kind of declaration that can be applied to classes, methods, accessor, property, or parameter.

Can I use decorators in TypeScript?

In TypeScript, you can create decorators using the special syntax @expression , where expression is a function that will be called automatically during runtime with details about the target of the decorator. The target of a decorator depends on where you add them.

How many decorators can be applied to a declaration?

It's possible to use as many decorators on the same piece of code as you desire, and they'll be applied in the order that you declare them. This defines a class and applies three decorators — two to the class itself, and one to a property of the class: @log could log all access to the class.


3 Answers

Decorators are called when the class is declared—not when an object is instantiated.

That's correct.

As @H.B. has already said, we can prove it by looking at the transpiled code.

var TestComponent = /** @class */ (function () {
    function TestComponent() {
        this.testString = "default string";
        console.log(this.testString);
    }
    __decorate([
        core_1.Input(),
        __metadata("design:type", String)
    ], TestComponent.prototype, "testString", void 0);
    TestComponent = __decorate([
        core_1.Component({
            selector: 'app-test',
            template: '{{testString}}'
        }),
        __metadata("design:paramtypes", [])
    ], TestComponent);
    return TestComponent;
}());

Now, let's go through the next steps to understand where you was wrong.

Step 1. The main purpose is to provide metadata

When I execute the code, the console logs "default string" instead of "altered string". This proves that decorators are called after the constructor of a class executes.

You can't be sure until you know what @Input() decorator does.

Angular @Input decorator just adorns component property with some information.

It's just metadata, that will be stored in TestComponent.__prop__metadata__ property.

Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA]

enter image description here

Step 2. Angular compiler.

Now its time when angular compiler collects all information about component including @Input metadatas to produce component factory. Prepared metadata looks like:

{
  "selector": "app-test",
  "changeDetection": 1,
  "inputs": [
    "testString"
  ],
  ...
  "outputs": [],
  "host": {},
  "queries": {},
  "template": "{{testString}}"
}

(Note: When Angular TemplateParser walk through template it uses this metadata to check whether directive has input with name testString)

Based on the metadata compiler constructs updateDirective expressions:

if (dirAst.inputs.length || (flags & (NodeFlags.DoCheck | NodeFlags.OnInit)) > 0) {
  updateDirectiveExpressions =
      dirAst.inputs.map((input, bindingIndex) => this._preprocessUpdateExpression({
        nodeIndex,
        bindingIndex,
        sourceSpan: input.sourceSpan,
        context: COMP_VAR,
        value: input.value
      }));
}

that will be included in producing factory:

enter image description here

We can notice above that update expressions are generated in parent view(AppComponent).

Step 3. Change detection

After angular produced all factories and inititialized all necessary objects it runs change detection cycly from top view through all child view.

During this process angular calls checkAndUpdateView function, where it also calls updateDirectiveFn:

export function checkAndUpdateView(view: ViewData) {
  if (view.state & ViewState.BeforeFirstCheck) {
    view.state &= ~ViewState.BeforeFirstCheck;
    view.state |= ViewState.FirstCheck;
  } else {
    view.state &= ~ViewState.FirstCheck;
  }
  shiftInitState(view, ViewState.InitState_BeforeInit, ViewState.InitState_CallingOnInit);
  markProjectedViewsForCheck(view);
  Services.updateDirectives(view, CheckType.CheckAndUpdate);  <====

That's the first place where your @Input property gets value:

providerData.instance[propName] = value;
if (def.flags & NodeFlags.OnChanges) {
  changes = changes || {};
  const oldValue = WrappedValue.unwrap(view.oldValues[def.bindingIndex + bindingIdx]);
  const binding = def.bindings[bindingIdx];
  changes[binding.nonMinifiedName !] =
    new SimpleChange(oldValue, value, (view.state & ViewState.FirstCheck) !== 0);
}

As you can see It happens before ngOnChanges hook.

Conclusion

Angular doesn't update @Input property value during decorator execution. Change detection mechanism is responsible for such things.

like image 150
yurzui Avatar answered Sep 27 '22 20:09

yurzui


The reason of your confusion is not caused by how Decorators works, but how Angular updates it's input-bound properties. You can prove yourself and

ngOnInit() {
  console.log(this.testString) // see updated value
}

This happens because ngOnInit is called after the first ngOnChanges and ngOnChanges updates your input.

like image 39
Julius Dzidzevičius Avatar answered Sep 27 '22 21:09

Julius Dzidzevičius


You can just look at the generated code, e.g.

const defaultValue = (value: any) =>
  (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    target[propertyKey] = value;
  };

class Test
{
  @defaultValue("steven")
  myProperty: string;

  constructor()
  {
    console.log(this.myProperty);
  }
}

new Test();

Will produce this:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var defaultValue = function (value) {
    return function (target, propertyKey, descriptor) {
        target[propertyKey] = value;
    };
};
var Test = /** @class */ (function () {
    function Test() {
        console.log(this.myProperty);
    }
    __decorate([
        defaultValue("steven")
    ], Test.prototype, "myProperty", void 0);
    return Test;
}());
new Test();

As you an see the __decorate function is called on the property at class declaration time. This redefines the property according to the decorator code. For Angular this probably just sets some metadata, priming the class for inputs. Here it directly sets the value.

Thus, here the property will already have changed in the constructor.

like image 43
H.B. Avatar answered Sep 27 '22 22:09

H.B.