Typescript's Accessor decorator ensures that the decorated code is executed only once, fetching the previously calculated value for any subsequent calls

The topic discussed here originates from a previous discussion on a method decorator in Typescript. In some scenarios, there are `get` methods in a Typescript class that involve intensive computations. Some of these methods always return the same result and do not change based on the instance's state. However, they are called multiple times on the same instance in the code. It would be efficient to execute the calculations only once, rather than every time the method is accessed. Here's an example:

class Cell {
    angle: number;
    count: number;
    private _cosine?: number;

    constructor(angle) {
        this.angle = angle;
        this.count = 0;
    }

    get cosine() {
        if (this.count) return this._cosine;
        this._cosine = Math.cos(this.angle);
        this.count++;
        return this._cosine;
    }
}

const cells = Array.from({ length: 100 }).map(
    (_, i) => new Cell(i * 180 * Math.PI)
);

cells.forEach((cell) => {
    for (i = 0; i < 100; i++) {
        const cosine = cell.cosine;
    }
});

When `cell.cosine` is first accessed, it runs the heavy computation and stores the result in the private property `_cosine`. Subsequent calls simply return this value. The `.count` for any `cell` remains at 1, even though `cell.cosine` is accessed 100 times per instance.

Implementing with a Decorator

The current approach requires creating a separate private property and including logic for each individual property needing the once-only behavior. There have been discussions about using decorators to achieve this optimization. While solutions exist for method decorators, adapting them for getters/accessors hasn't been straightforward as getters need to return a value every time they're called.

Is there a way to create a decorator specifically for getter methods that executes the computation only on the first call and returns the calculated value thereafter?

Addressing Potential Questions

You might wonder why not assign the value in the constructor instead? While ideal for performance improvement, certain scenarios like the one below may make this approach impractical:

class Cell {
    position: number;
    count: number;

    constructor(position) {
        this.position = position;
        this.count = 0;
    }

    get neighbors() {
        let neighbors = [];
        for (let j = -1; j <= 1; j++) {
            for (let i = -1; i <= 1; i++) {
                neighbors.push(
                    new Cell([x + i, y + j]),
                );
            }
        }
        return neighbors;
    }
}

In cases like this, where calling `cell.neighbor` creates multiple new instances, assigning `this.neighbors` in the constructor triggers an infinite loop. Although manually implementing a check for already computed values is a solution, utilizing a decorator appears more elegant. Performance tests indicate a slight enhancement by incorporating this decorator (though not as significant as initializing in the constructor).

Answer №1

One approach is to calculate the value for the first time, store it, and then access that stored value for subsequent calls by utilizing reflect-metadata.

import "reflect-metadata";

const metadataKey = Symbol("initialized");

function once(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const getter = descriptor.get!;
  descriptor.get = function () {
    const val = Reflect.getMetadata(metadataKey, target, propertyKey);

    if (val) {
      return val;
    }

    const newValue = getter.apply(this);
    Reflect.defineMetadata(metadataKey, newValue, target, propertyKey);
    return newValue;
  };
}

You can see a working demo here.

Kudos to the original poster for helping resolve any issues.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Even when it appears to be chaotic, the TypeScript array of numbers always manages to find its way back to being sorted

I recently developed a function that aims to verify if an array of numbers is sorted: const checkIfSorted = (numbers: number[]) => { return numbers === numbers.sort((a, b) => a - b); }; checkIfSorted([4, 2, 8, 7, 3, 10, 1, 5, 9, 6]); // This cur ...

You cannot call this expression. The data type 'Boolean' does not have any callable signatures

As I delve into learning a new set of technologies, encountering new errors is inevitable. However, there is one particular type of error that keeps cropping up, making me question if I am approaching things correctly. For instance, I consistently face t ...

Outputting undefined values when processing an http post array

I seem to have encountered a major issue. Despite my efforts, I am seeing an undefined value when trying to display this JSON data. {"StatusCode":0,"StatusMessage":"OK","StatusDescription":{ "datas": [ {"sensor_serial":"SensorSerial1", "id":"11E807676E3F3 ...

Encountered an Angular 2 error: NullInjectorError - Http provider not found

I've encountered an issue while trying to access a JSON GitHub service, receiving the error message NullInjectorError: No provider for Http! Although I've attempted to add providers throughout the code, my efforts have been unsuccessful. I' ...

Using the tensorflow library with vite

Greetings and apologies for any inconvenience caused by my relatively trivial inquiries. I am currently navigating the introductory stages of delving into front-end development. Presently, I have initiated a hello-world vite app, which came to life throug ...

What is the reason for the manual update of a view when copying an object's attributes into another object, as opposed to using Object.assign()?

In my application, I have a parent component called 'EmployeeComponent' that is responsible for displaying a list of employees. Additionally, there is a child component named 'EmployeeDetailComponent' which displays the details of the s ...

Define a new type in Typescript that is equal to another type, but with the added flexibility of having optional

I have 2 categories: category Main = { x: boolean; y: number; z: string } category MainOptions = { x?: boolean; y?: number; z?: string; } In this scenario, MainOptions is designed to include some, none, or all of the attributes that belong to ...

Navigate to a new tab using this.router.navigate

Is there a way to redirect the user to a specific page with ${id} opening in a new tab, after clicking a button in an angular material dialog box? I want to leave the dialog box open while querying the new page. Currently, the redirect happens but not in a ...

Implementing the breadcrumb component within dynamically loaded modules loaded through the router-outlet component

I'm currently working on an angular 8 breadcrumb component for my application. The requirement is to display it in the content of the page, not just in the header, and it should never be located outside the router-outlet. This has posed a challenge fo ...

Creating Algorithms for Generic Interfaces in TypeScript to Make them Compatible with Derived Generic Classes

Consider the (simplified) code: interface GenericInterface<T> { value: T } function genericIdentity<T>(instance : GenericInterface<T>) : GenericInterface<T> { return instance; } class GenericImplementingClass<T> implemen ...

Automatically convert TypeScript packages from another workspace in Turborepo with transpilation

I have set up a Turborepo-based monorepo with my primary TypeScript application named @myscope/tsapp. This application utilizes another TypeScript package within the same repository called @myscope/tspackage. For reference, you can view the example reposit ...

Console not logging route changes in NextJS with TypeScript

My attempt to incorporate a Loading bar into my NextJs project is encountering two issues. When I attempt to record a router event upon navigating to a new route, no logs appear. Despite my efforts to include a loading bar when transitioning to a new rout ...

Accessing Nested Arrays in Angular 8: Retrieving Data in HTML Template from Multiple Layers of Arrays

Hello there. I'm using an API that gives me the following data: (4) [{…}, {…}, {…}, {…}] 0: dueDate: "2018-03-26T00:00:00" priority: {priorityId: 1, priorityName: "Critical", priorityColor: "red"} statuses: Array(1) 0: ...

Using React's higher order component (HOC) in TypeScript may trigger warnings when transitioning from non-TypeScript environments

I have a simple HOC component implemented in React with TypeScript. export const withFirebase = <P extends object>( Component: React.ComponentType<P> ) => class WithFirebase extends React.Component<P> { render() { return ...

Previous states in TypeScript

Just starting out with typescript and trying to work with user files in order to update the state. Currently facing a typescript error that I can't seem to figure out - Error message: Argument of type '(prev: never[]) => any[]' is not as ...

React-query: When looping through useMutation, only the data from the last request can be accessed

Iterating over an array and applying a mutation to each element array?.forEach((item, index) => { mutate( { ...item }, { onSuccess: ({ id }) => { console.log(id) }, } ); }); The n ...

A guide on showcasing nested arrays data in an Angular application

info = [ { list: [ { title: 'apple'} ] }, { list: [ { title: 'banana'} ] } ] My goal here is to extract the list items. Here is how they are structured. desired r ...

A TypeScript function that returns the ReturnType of a specific callback function

Is it possible to define an annotation for a function that accepts a callback, and have the function return type determined by the callback's return type? // Suppose the callback takes a number as argument function processCallback(cb: (arg:number) =&g ...

The element is implicitly imparted with an 'any' type due to the incapability of utilizing an expression of type 'number' to index the type '{}'. This error occurs in the context of VUEJS

I have encountered an issue that I have been struggling to resolve despite trying numerous solutions. The problem arises while working on a project using Vue. Here is how I have structured my data: data(){ return{ nodes: {}, edges:{}, ...

What could be causing the error message (No overload matches this call) to pop up when attempting to subscribe to .valueChanges() in order to retrieve data from Firestore?

Currently, I am developing an Angular application that utilizes Firebase Firestore database through the angularfire2 library. However, I am encountering a challenge. I must admit that my background is more in Java than TypeScript, so there might be some g ...