Method decorator that allows to execute a decorated method only once Typescript

I have a task to Implement a method decorator that allows to execute a decorated method only once.
For example:

   class Test {
        data: any;
        @once
        setData(newData: any) {
            this.newData = newData;
        }
    }
    const test = new Test();
    test.setData([1,2,3]);
    console.log(test.data); // [1,2,3]
    test.setData('new string');
    console.log(test.data); // [1,2,3]  

I tried a lot of combinations to make a function that being called twice do nothing,but it's not what i should have and unit tests are failing,so,this is what i have till now:

const once = (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
) => {
    const method = descriptor.value;
    
    descriptor.value = function (...args){
        // ???
    }
};  

Unit tests:

describe('once', () => {
    it('should call method once with single argument', () => {
        class Test {
            data: string;
            @once
            setData(newData: string) {
                this.data = newData;
            }
        }
        const test = new Test();
        test.setData('first string');
        test.setData('second string');
        assert.strictEqual(test.data, 'first string')
    });

    it('should call method once with multiple arguments', () => {
        class Test {
            user: {name: string, age: number};
            @once
            setUser(name: string, age: number) {
                this.user = {name, age};
            }
        }
        const test = new Test();
        test.setUser('John',22);
        test.setUser('Bill',34);
        assert.deepStrictEqual(test.user, {name: 'John', age: 22})
    });

    it('should return always return first execution result', () => {
        class Test {
            @once
            sayHello(name: string) {
                return `Hello ${name}!`;
            }
        }
        const test = new Test();
        test.sayHello('John');
        test.sayHello('Mark');
        assert.strictEqual(test.sayHello('new name'), 'Hello John!')
    })
});  

Could you help me please? Thanks in advance!


Solution 1:

reflect-metadata is pretty handy for this scenario. You could try something like this:

import 'reflect-metadata';

const metadataKey = Symbol('initialized');

export function once(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const method = descriptor.value;
    descriptor.value = function(...args) {
        const initialized = Reflect.getMetadata(metadataKey, target, propertyKey);

        if (initialized) {
            return;
        }

        Reflect.defineMetadata(metadataKey, true, target, propertyKey);

        method.apply(this, args);
    }
}

You can find some more information on it here: https://www.typescriptlang.org/docs/handbook/decorators.html#metadata

Solution 2:

Try something like this:

const once = (
  target: Object,
  propertyKey: string | symbol,
  descriptor: PropertyDescriptor
) => {
  const method = descriptor.value;
  let isFirstTime = true;

  descriptor.value = function (...args: any[]) {
    if (!isFirstTime) { return; }
    isFirstTime = false;
    method(...args);
  }
};