typescripttypescript-decorator

why is typescript overriding property decorator with undefined value


I am trying out decorators and wanted to create a library. But I got stuck with an unexpected behavior.

Why is this property decorator overridden by the construction? What is happening behind the scene that make the property definition change from a getter/setter to value type property.

This is really confusing, since as I know during the construction it was not even supposed to create the field with undefined value, since the only value given to this field is 1 from the decorator, and it is a getter.

PS: I have tried typescript 4.9.5 and 5.1.6 same result.

function description(target, key, title?: string) {
  const desc = Object.getOwnPropertyDescriptor(target, key);
  console.log(title ?? 'description', desc);
  return desc;
}

function FakeFactory<T = any>() {
  return function (target: any, propertyKey: any) {
    let _value: any = 1;

    description(target, propertyKey, 'before');
    // console.log: before undefined

    Object.defineProperty(target, propertyKey as string, {
      get() {
        return _value;
      },
      set(newValue: T) {
        _value = newValue;
      },
      enumerable: true,
      configurable: true,
    });

    description(target, propertyKey, 'after');
    // console.log: after 
    // { get: [λ: get], 
    //   set: [λ: set], 
    //   enumerable: true, 
    //   configurable: true }
  };
}

function Deco() {
  return FakeFactory();
}

class A {
  constructor() {
    description(this, 'field', 'constructor');
    // console.log: constructor
    // { value: undefined, 
    //   writable: true, 
    //   enumerable: true, 
    //   configurable: true } 
  }

  @Deco()
  field: string;
}
const a = new A();
description(a, 'field', 'final');
// console.log: final
// { value: undefined, 
//   writable: true, 
//   enumerable: true, 
//   configurable: true }

// Expected field definition  
// { get: [λ: get], 
//   set: [λ: set], 
//   enumerable: true, 
//   configurable: true }

tsconfig of this code

{
  "compilerOptions": {
    "esModuleInterop": true,
    "lib": [
      "esnext"
    ],
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "incremental": true,
    "allowJs": true,
    "isolatedModules": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,

    "strictPropertyInitialization": false,
    "allowSyntheticDefaultImports": true,
  }
}

Solution

  • The "target" parameter of before description(target.., "before") it's the A class constructor itself and the "target" parameter of after description(target.., "after") is an object because defineProperty add or modify property and return object

    Check demo (Enable experimental support for TC39 stage 2 draft decorators.) :

    import "reflect-metadata";
    
    function description(target: any, key: any, title?: string) {
    
      const desc = Reflect.getOwnPropertyDescriptor(target, key);
      console.log(title ?? 'description', desc);
      return desc;
    
    }
    
    function format(formatString: string) {
      return (target: any, propertyKey: string) => {
    
        description(new target.constructor(), propertyKey, 'before');
    
        Reflect.defineProperty(target.constructor.prototype, propertyKey, {
          get() {
            return formatString?.replace("%s", this._value);
          },
          set(newValue: any) {
            this._value = newValue;
          },
          enumerable: true,
          configurable: true,
        })
    
        description(target, propertyKey, 'after');
    
      }
    }
    
    function Deco(formatString: string) {
      return format(formatString);
    }
    
    class Greeter {
      @Deco("Hello, %s")
      greeting: string = "demo";
      constructor(message: string) {
        this.greeting = message;
      }
    }
    
    const greeter = new Greeter('world');
    
    console.log(greeter.greeting);
    

    result :

    [LOG]: "before",  {
      "value": undefined,
      "writable": true,
      "enumerable": true,
      "configurable": true
    } 
    [LOG]: "after",  {
      "enumerable": true,
      "configurable": true
    } 
    [LOG]: "Hello, world" 
    

    You can't return descriptor form property decorator because the property definitions doesn't change when calling description function but the code still working (not recommended):

    // i declare custom Property Descriptor type equal to any
    type TPropertyDescriptor = any
    
    function description(target: any, key: any, title?: string) {
    
      const desc = Object.getOwnPropertyDescriptor(new target.constructor(), key);
      console.log(title ?? 'description', desc);
      return desc;
    
    }
    
    function format(formatString: string) {
      return (target: any, propertyKey: string) => {
    
        description(target, propertyKey, 'before');
    
        let _value: string = '';
    
        const getter = function () {
          return formatString?.replace("%s", _value);;
        };
    
        // Set the modifiedMessage value
        const setter = function (newValue: string) {
          _value = newValue
        };
    
    
        const descriptor: TPropertyDescriptor = {
          set: setter,
          get: getter,
          configurable: true
        }
    
    
        description(target, propertyKey, 'after');
    
        // return descriptor
        return descriptor;
      }
    }
    
    function Deco(formatString: string) {
      return format(formatString);
    }
    
    class Greeter {
      @Deco("Hello, %s")
      greeting?: string;
      constructor(message: string) {
        this.greeting = message;
      }
    }
    
    const greeter = new Greeter("world");
    
    console.log(greeter.greeting);
    

    output :

    [LOG]: "before",  {
      "value": undefined,
      "writable": true,
      "enumerable": true,
      "configurable": true
    } 
    [LOG]: "after",  {
      "value": undefined,
      "writable": true,
      "enumerable": true,
      "configurable": true
    } 
    [LOG]: "Hello, world"