The combination of TypeScript decorators and Reflect metadata is a powerful tool for

Utilizing the property decorator Field, which adds its key to a fields Reflect metadata property:

export function Field(): PropertyDecorator {
    return (target, key) => {
        const fields = Reflect.getMetadata('fields', target) || [];
        if (!fields.includes(key)) {
            fields.push(key)
        }
        Reflect.defineMetadata('fields', fields, target)
    }
}

An abstract base-class Form then accesses this metadata through a getter method:

abstract class Form {
    get fields() {
        return Reflect.getMetadata('fields', this) || [];
    }
}

This setup allows for distinguishing form fields from other class properties. Example usage with these classes:

abstract class UserForm extends Form {
    @Field()
    public firstName: string

    @Field()
    public lastName: string

    get fullName() {
        return this.firstName + ' ' + this.lastName;
    }
}

class AdminForm extends UserForm {
    @Field()
    roles: string[]
}

const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName']

However, an issue arises when creating a sister class to AdminForm - MemberForm. When multiple subclasses exist for Form, the fields getter returns all fields:

class MemberForm extends UserForm {
    @Field()
    memberSince: Date;
}

const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName', 'memberSince'] <--!!!

This behavior seems unexpected. Why does the memberSince field show up on an instance of AdminForm? How can different fields be defined for different subclasses?

Answer №1

The issue lies in the behavior of getMetadata, which traverses down the prototype chain and retrieves the value defined on the base type. To overcome this, utilize getOwnMetadata to specifically fetch the array field of the current class when adding a new field. When retrieving fields, ensure to navigate up the property chain to include all base class fields.

Consider implementing the following solution:

import 'reflect-metadata'
export function Field(): PropertyDecorator {
  return (target, key) => {
      const fields = Reflect.getOwnMetadata('fields', target) || [];
      if (!fields.includes(key)) {
          fields.push(key)
      }
      Reflect.defineMetadata('fields', fields, target)
  }
}

abstract class Form {
  get fields() {
      let fields = []
      let target = Object.getPrototypeOf(this);
      while(target != Object.prototype) {
        let childFields = Reflect.getOwnMetadata('fields', target) || [];
        fields.push(...childFields);
        target = Object.getPrototypeOf(target);
      }
      return fields;
  }
}

abstract class UserForm extends Form {
  @Field()
  public firstName!: string

  @Field()
  public lastName!: string

  get fullName() {
      return this.firstName + ' ' + this.lastName;
  }
}

class AdminForm extends UserForm {
  @Field()
  roles!: string[]
}

const form1 = new AdminForm()
console.log(form1.fields) // ['roles', 'firstName', 'lastName']

class MemberForm extends UserForm {
  @Field()
  memberSince!: Date;
}

const form2 = new MemberForm()
console.log(form2.fields) // ["memberSince", "firstName", "lastName"]

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

How can Firebase and Ionic be used to customize the password reset template for sending verification emails and more?

I'm facing an issue with firebase's auth templates not supporting my native language. Is there a way to customize the password reset template to also handle verification and email address change emails? ...

The term 'string' is typically employed as a data type, yet in this instance it is being utilized as an actual value

Just started working with TypeScript and encountered an issue while trying to set the state. Encountered this error message: 'string' is a type and cannot be used as a value here. const state = reactive({ user: { uid: "", ...

The module cannot be located due to an error: Package path ./dist/style.css is not being exported from the package

I am facing an issue with importing CSS from a public library built with Vite. When I try to import the CSS using: import 'rd-component/dist/style.css'; I encounter an error during the project build process: ERROR in ./src/page/photo/gen/GenPhot ...

What is the best way to simulate a dynamoDB call using jest?

In a simple Handler, I have calls to getData defined in a separate file export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { let respData = await new DynamoDBClient().getData(123); return { status ...

`What is the best way to transfer an observable to a child component?`

I am facing an issue with passing an Observable down to a child component. I have tried various solutions but none seem to work. parent.component.ts: export class ParentComponent { items$ = of([{name: "Item 1"}, {name: "Item 2"}]); } ...

Is there a way to track the loading time of a page using the nextjs router?

As I navigate through a next.js page, I often notice a noticeable delay between triggering a router.push and the subsequent loading of the next page. How can I accurately measure this delay? The process of router push involves actual work before transitio ...

Converting an array into an object using Typescript and Angular

I have a service that connects to a backend API and receives data in the form of comma-separated lines of text. These lines represent attributes in a TypeScript class I've defined called TopTalker: export class TopTalker { constructor( pu ...

Leveraging npm for the development of my TypeScript/Node.js project

I'm facing challenges working on a project written in TypeScript and running on Node. I am finding it difficult to write the npm script to get it up and running properly for development purposes. What I am attempting to achieve is: clear the /dist f ...

lint-staged executes various commands based on the specific folder

Within my project folder, I have organized the structure with two subfolders: frontend and backend to contain their respective codebases. Here is how the root folder is set up: - backend - package.json - other backend code files - frontend - p ...

Extracting values from an *ngFor loop in Angular 8 - Here's how to do

Currently, I am utilizing *ngFor to display some data. I have a requirement to extract a specific value from *ngFor and display it in, for instance, my heading. However, when I attempted to use {{ project }}, it consistently returned undefined. Despite hav ...

Filtering database results from an Angular component

I am currently working on an Angular component and I have a result variable in the .ts file that stores data retrieved from the database. My goal is to filter this result variable to display only 20 records and sort them by date either in ascending or de ...

What makes fastify-plugin better than simply calling a regular function?

I recently came across a detailed explanation of how fastify-plugin operates and its functionality. Despite understanding the concept, I am left with a lingering question; what sets it apart from a standard function call without using the .register() metho ...

React: Why aren't class methods always running as expected?

I created a class component that retrieves a list of applications from an external API. It then sends a separate request for each application to check its status. The fetching of the applications works well, but there is an issue with the pinging process ...

Creating a web application using Aframe and NextJs with typescript without the use of tags

I'm still trying to wrap my head around Aframe. I managed to load it, but I'm having trouble using the tags I want, such as and I can't figure out how to load a model with an Entity or make it animate. Something must be off in my approach. ...

Encountered an unexpected token error when executing karma-coverage in a project using TypeScript

I have been working on a simple Angular/Typescript project that includes 12 basic unit tests which all pass successfully. However, I am now looking to measure the code coverage of these tests. Despite trying different methods, I have not been able to achie ...

Issue with Typescript - Node.js + Ionic mobile app's Angular AoT build has encountered an error

Currently, I am in the process of developing an Android application using Node.js and Ionic framework. The app is designed to display random text and images stored in separate arrays. While testing the app on Chrome, everything works perfectly fine. Upon ...

SonarQube flagging a suggestion to "eliminate this unnecessary assignment to a local variable"

Why am I encountering an error with SonarQube? How can I resolve it since the rule page does not offer a specific solution? The suggestion is to eliminate the unnecessary assignment to the local variable "validateAddressRequest". validateAddress() { ...

Opening a modal in Angular2+ when selecting an item from ngx-chips (tag-input)

I need to implement a functionality where clicking on a tag in a dropdown should trigger the opening of a modal window in my Angular application. Below is the code snippet related to this feature: <div class="force-to-the-bottom"> <tag-input [ ...

Tips for ensuring that CSS hover effects stay in place even when the page is scrolled

i'm having trouble with a project in React JS that I received from another team. I'm not very confident in my skills with React JS, and I'm facing an issue where a certain part of the page is supposed to change to red when hovered over. Howe ...

typescriptIn React Router v5 with TypeScript, an optional URL parameter is implemented that can have an undefined

I'm currently working with react-router v5.1 and TypeScript, and I have set up the following route configurations: <Router basename="/" hashType="slash"> <Switch> <Route path="/token/:tokenName"> <TokenPag ...