Guide on making an "enumerable" wrapper for accessors in TypeScript

I've been exploring a method to create an @enumerable decorator that can expose properties set through accessor methods.

It's quite simple to achieve this for instances of a class:

// This function works well when called in the class constructor like makeEnumerable(this, ['prop1', 'prop2'])
const makeEnumerable = (what: any, props: string[]) => {
  for (const property of props) {
    const descriptor = Object.getOwnPropertyDescriptor(what.constructor.prototype, property);
    if (descriptor) {
      const modifiedDescriptor = Object.assign(descriptor, { enumerable: true });
      Object.defineProperty(what, property, modifiedDescriptor);
    }
  }
};

However, transforming this into a decorator seems challenging because the function lacks access to the instance.

// Doesn't work with Object.keys, Object.getOwnPropertyNames, or Object.entries
function enumerable (value: boolean = true): any {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
    if (descriptor) {
      Object.assign(descriptor, { enumerable: value });
    }
  };
}

The property still can be enumerated within for (const x in y) constructs (oddly), but not elsewhere - and unfortunately, Object.entries throws an error.

Below is an example using the aforementioned functions:

class MyClass {
  #privateVal1: any;
  #privateVal2: any;

  constructor () {
    makeEnumerable(this, ['b']);
  }

  @enumerable(true)
  get a () {
    return this.#privateVal1;
  }

  set a (val: any) {
    this.#privateVal1 = val;
  }

  get b () {
    return this.#privateVal2;
  }

  set b (val: any) {
    this.#privateVal2 = val;
  }
}

const enumerableA = new MyClass();
enumerableA.a = 5;
enumerableA.b = 6;

const keys = [];
for (const key in enumerableA) {
  keys.push(key);
}

console.log({
  'forin': keys, // ['a', 'b']
  'keys': Object.keys(enumerableA), // ['b']
  'keys(proto)': Object.keys(Object.getPrototypeOf(enumerableA)), // ['a']
  'getOwnPropertyNames': Object.getOwnPropertyNames(enumerableA), // ['b']
  'getOwnPropertyNames(proto)': Object.getOwnPropertyNames(Object.getPrototypeOf(enumerableA)), // ['constructor', 'a', 'b']
});

console.log({
  'entries': Object.entries(enumerableA), // Error('Cannot read private member #privateVal1 from an object whose class did not declare it');
  'entries(proto)': Object.entries(Object.getPrototypeOf(enumerableA)), // Error('Cannot read private member #privateVal1 from an object whose class did not declare it');
});

Is there a way to utilize a decorator to transform an accessor method into an enumerable property?

Answer №1

The Difference Between Prototype and Instance Properties

It's important to grasp the concept of Prototype vs Instance properties

  • makeEnumerable applies enumerable descriptors at the instance level.
  • enumerable decorator alters prototype-level descriptors.

When you expect Object.keys(enumerableA) to be ['a', 'b'], keep in mind:

  • The for...in loop goes through both own and inherited enumerable properties.
  • Meanwhile, Object.keys only returns its own enumerable properties.

https://i.sstatic.net/2f3RlEM6.png https://i.sstatic.net/TpsjwoxJ.png

For further information, refer to this MDN blog: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties#querying_object_properties

Explanation of Different Outputs

for (const key in enumerableA)

  • Output: ['a', 'b']
  • Reasons:
    • The for...in loop covers both own and inherited enumerable properties.
    • b is set as enumerable by makeEnumerable on the instance, while a becomes enumerable at the prototype level via the @enumerable decorator.

Object.keys(enumerableA)

  • Output: ['b']
  • Reasons:
    • Object.keys shows only the own enumerable properties.
    • b is an own enumerable property due to makeEnumerable in the constructor.
    • a remains on the prototype, hence excluded.

Object.keys(Object.getPrototypeOf(enumerableA))

  • Output: ['a']
  • Reasons:
    • This lists solely enumerable properties at the prototype level; @enumerable decorator adjusts the descriptor for a.
    • b is non-enumerable at the prototype, as it was made enumerable on the instance only by makeEnumerable function.

Object.getOwnPropertyNames(enumerableA)

  • Output: ['b']
  • Reasons:
    • All own properties (both enumerable and non-enumerable) are listed but prototype properties are ignored.
    • b is an own property on the instance.

Object.getOwnPropertyNames(Object.getPrototypeOf(enumerableA))

  • Output: ['constructor', 'a', 'b']
  • Reasons:
    • This shows all own properties (both enumerable and non-enumerable) defined directly on the prototype.
    • constructor and b are non-enumerable but present on the prototype.

Why Does Object.entries Cause an Error?

Object.entries accesses all enumerable own properties.

  • During Object.entries(enumerableA):
    • It accesses b as an enumerable property at the instance level. The context of this when accessing b within get b() {...} is the MyClass { b: [Getter/Setter] } instance.
    • This functionality operates smoothly because private properties within the class are accessible when this references the instance.
  • However, with
    Object.entries(Object.getPrototypeOf(enumerableA))
    :
    • It targets a as it's an enumerable property at the prototype level.
    • An error is encountered because this within get a(){...} refers to the prototype object ({ a: [Getter/Setter] }) rather than an instance of MyClass.

Understanding how typescript handles private properties is crucial.

  • Typescript generates functions to verify this during method calls. If this doesn't match the own class, an error is thrown.
  • Refer to the compiled Javascript code for additional insights.

Can a Decorator Make an Accessor Method an Enumerable Property?

No, utilizing decorators in Typescript to directly make instance properties enumerable isn't feasible because property decorators in Typescript only interact with the class prototype for instance members, not the instances themselves.

  • A Typescript property decorator functions before any class instances are created, operating at the class definition level.
  • To render instance properties enumerable, employ the makeEnumerable function similar to how b was handled.



Your concerns have hopefully been addressed comprehensively. Should you require further clarifications, please don't hesitate to inquire. Happy learning!

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

Create a feature that allows users to view photos similar to the way it

I want to incorporate a Twitter feed on my website that displays images posted. Instead of having the images redirecting users to Twitter when clicked, I would like them to reveal themselves underneath when a link labeled "View Photo" is clicked, and then ...

Designing a dynamic webpage using JavaScript to seamlessly display the latest updates in real-time

I am interested in developing a web page that enables users to input new data and perform calculations without refreshing the entire page. I prefer a table format for simplicity, but ease of coding is also important. What would be the most efficient approa ...

Tips for Positioning Ant Design Select Dropdown Anchor to the Right of the Select Component

Currently, I am utilizing Ant Design in my project and encountering an issue with a Select component. The Select is positioned to the right side of the screen and contains labels that are quite long. This causes the dropdown to overflow and a scrollbar t ...

Exploring Elasticsearch: Uncovering search results in any scenario

I've been working on a project where my objective is to receive search engine results under all conditions. Even if I enter a keyword that is not included in the search data or if it is an empty string, I still want to get some kind of result. How can ...

Design an interactive quarter-circle using CSS styling

My goal is to create this element using CSS, however, the content inside needs to be dynamic. I initially attempted to use border-radius, but realized it is unable to achieve the desired outcome. If anyone has a solution or can offer assistance, I would g ...

Is there a way to deactivate the vote buttons once a user has placed their vote?

Currently, I am in the process of implementing a voting system for the comment section of my website. To achieve this, I have set up two database tables - one named comments to store the score of each comment, and another named votes to keep track of indiv ...

You can only set headers once during the initial request; any additional attempts to set headers will result in an

I encountered a specific issue with the error message "Can't set headers after they are sent". Here is the relevant code snippet: create: (request, response, next) -> socket = @app.socket # # This method will be used to call the right method ins ...

Is there a method to measure the time it takes to download a script apart from its actual execution time

I am looking to track timing variables for my primary JavaScript load. One approach could be: <script> performance.mark('script-start') </script> <script src="something.js"></script> Later in something.js, inc ...

Playing a game of rock, paper, scissors with two players using JavaScript

Hello! I am a beginner in JavaScript and I am trying to create a simple rock, paper, scissors game. However, when I run the code, I receive two prompt messages and an error saying 'TypeError: playerOneChoice is not a function'. What mistake did I ...

Struggling to minimize space between icon buttons within a vertical layout (Bootstrap, HTML, and CSS)

My goal was to align the icon buttons with the dynamic table on their right side, but they are overflowing slightly instead. Desired look / Current appearance . Before sharing my code block, I experimented with various options: Adjusting padding, margin, ...

The specified type 'Observable<string | null>' cannot be assigned to type 'string'

I am struggling with the following code: id$!: Observable<string | null>; characterDetail$!: Observable<CharacterData | undefined>; //?????? constructor( private router: ActivatedRoute, private store$: Store ) {} ngOnInit(): void ...

Modify the color of the object model when clicked - Three.js

When a user clicks on a specific part of the object model, I want to display the wireframe of that part to indicate which part is being modified. The user can then choose a color for that part from a palette. However, the line child.material.color.set(se ...

Expo background fetch initialized but not activated

During the development of my React Native app, I encountered the need to perform periodic background fetches from another server. To achieve this, I utilized two classes from Expo: import * as BackgroundFetch from 'expo-background-fetch'; import ...

Updating ViewModel and refreshing the view following an AJAX request

I need assistance in creating a table where each row has a child row serving as a details section. This details section should display a log history and allow users to input new logs. Upon adding a new log by clicking the "Add" button, the log history shou ...

Copying to the clipboard now includes the parent of the targeted element

Edit - More Information: Here is a simplified version of the sandbox: https://codesandbox.io/s/stupefied-leftpad-k6eek Check out the demo here: https://i.sstatic.net/2XHu1.jpg The issue does not seem to occur in Firefox, but it does in Chrome and other ...

Tips for obtaining the HTML document as a string within an Android web browser

I am trying to retrieve the source code (HTML document) of a webpage when it loads in my code. I have written a function onPageFinished() with view.loadUrl("javascript:(function() { document.getElementByTagName('html')[0].innerHTML"); to get the ...

Obtain the response body in Nest JS middleware

Currently, I am working on developing logging middleware for my project. My main goal is to log the response body specifically in 500 responses. However, I have encountered an issue where the response body is not present in the Response object when using a ...

Changing the value in a textbox by altering a select option involves adjusting the input based on the selected

Is there a way to update the input value when a new option is chosen from a select dropdown menu? Click here to view an image. The goal is to have a different number of image upload fields based on the selected ad package (e.g., free package has 0 images ...

Node corrupting images during upload

I've been facing an issue with corrupted images when uploading them via Next.js API routes using Formidable. When submitting a form from my React component, I'm utilizing the following functions: const fileUpload = async (file: File) => ...

jQuery - patience is required until all elements have completely faded away

I am facing a unique challenge: I need to trigger an action after two specific elements have been faded out simultaneously. The code snippet for this task is as follows: $("#publish-left, #publish-right, #print-run").fadeOut(function(){ //do something ...