How can I use the target type (and maybe even the property type) as a type parameter within a decorator?

In the process of incorporating a deep-watch property decorator in Angular, the following usage has been implemented:

@Component({ /* ... */ })
export class AppComp {

  @Watch(
    'a.b.c',
    function (last, current, firstChange) {
      // callback
    }
  )
  prop = { a: { b: { c: 'Hello' } };
}

Most of the implementation is complete, however, the callback lacks specific types. The this type is currently any, and both last and current parameters are also defined as any.

To provide these types to the callback function through generics, the following approach can be applied:

type PropType = { a: { b: { c: string } };

@Component({ /* ... */ })
export class AppComp {

  //    ↓ generic here
  @Watch<AppComp, PropType>(
    'a.b.c',
    function (last, current, firstChange) {
      // callback
      // this -> AppComp
      // last -> PropType
      // current -> PropType
    }
  )
  prop: PropType = { a: { b: { c: 'Hello' } };
}

While effective, this approach may seem verbose given that it's clear in this context that the callback function should have AppComp for this and PropType for last and current parameters.

A simplified version of the Watch definition is provided below:

interface IWatchDirectiveTarget {
  ngDoCheck?: Function;
}

interface CallbackFunction<ThisType, WatchedType> {
  (this: ThisType, last: WatchedType, current: WatchedType, firstChange: boolean): any;
}

export function Watch<ThisType, WatchedType> (
  path: string,
  cb: CallbackFunction<ThisType, WatchedType>
): (
  target: IWatchDirectiveTarget,
  key: string
) => void {
  return function (
    target: IWatchDirectiveTarget,
    key: string
  ) {
    target.ngDoCheck = function () {
      // some change detecting logic...
      cb.call(this, lastValue, currentValue, isFirst);
    };
  }
}

The challenge lies in passing types into Watch and subsequently from Watch into CallbackFunction. There's a desire for a more streamlined approach using macros or similar concepts represented by placeholders like __CURRENT_CLASS_TYPE__ or __CURRENT_PROPERTY_TYPE__:

@Watch<__CURRENT_CLASS_TYPE__, __CURRENT_PROPERTY_TYPE__>(path, cb)

Although not aesthetically pleasing, grouping

@Watch<__CURRENT_CLASS_TYPE__, __CURRENT_PROPERTY_TYPE__>
together could potentially simplify things and detach them from specific class and property names.

Alternatively, incorporating macro-like elements within the Watch definition could eliminate the need for external generic type parameters:

function Watch (
  path: string,
  cb: CallbackFunction<__CURRENT_CLASS_TYPE__, __CURRENT_PROPERTY_TYPE__>
): (
  target: IWatchDirectiveTarget,
  key: string
) => void {
  return function (
    target: IWatchDirectiveTarget,
    key: string
  ) {
    target.ngDoCheck = function () {
      // some change detecting logic...
      cb.call(this, lastValue, currentValue, isFirst);
    };
  }
}

Is there a feasible way to achieve this level of abstraction?

Edit:

Reference has been made to: TypeScript property decorator that takes a parameter of the decorated property type

There seems to be potential for establishing a connection between the value provided to the decorator factory and the type of the decorated property...

Answer №1

Attempting to provide a partial answer to my question, I have followed the approach outlined in a post about TypeScript property decorators. By tweaking the definition from a decorator factory to a decorator factory factory, I was able to eliminate the need for specifying the target type directly:

New Definition:

interface Fn<TClass, TValue> {
  (this: TClass, last: TValue, current: TValue, firstChange: boolean): any;
}

export function DecoratorFactoryFactory<TValue> ():
  <TClass>(pathOrCb: string | Fn<TClass, TValue>, _cb?: Fn<TClass, TValue>) =>
  (target: TClass, key: string) =>
  void
{
  return function (pathOrCb, _cb) {
    return function (target, key) {
      // Modify change detection logic...
    }
  }
}

Implementation:

@Component({ /* ... */ })
export class AppComponent {

  // Two calls required here, with the first returning the decorator factory and the second returning the actual decorator
  @DecoratorFactoryFactory<number[] | undefined>()(
    'nestedProp',
    function(last, current, firstChange) {
      console.log('Debugging change detected', this, last, current, firstChange);
    }
  )
  public prop;
}

While I refer to this as a "half" solution due to still needing to specify the value type, I find it acceptable since I utilize a string literal to indicate the value's path. TypeScript does not currently support inferring types based on string literals.

  1. Automatic type inference via property chaining + optional properties
  2. Manual type specification plus providing the value

Determining which approach is superior can be challenging.

This workaround has its drawbacks - notably the two-step process in usage that may confuse users of the decorator. As such, I am withholding acceptance of my own solution in hopes that someone proficient in TypeScript can devise more streamlined or automated solutions for type inference.

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

Utilizing BehaviourSubject for cross-component communication in Angular

My table is populated with data from a nodeJS API connected to methods inside my service. I attempted using a Behavior Subject in my service, initialized as undefined due to the backend data retrieval: Service: import { Injectable } from "@angular/core" ...

Issues arise when attempting to override attributes within the HTML of a parent component in Angular

Why does overriding an attribute in a child class that extends from another not work as expected? Here's a made-up scenario to simplify the issue: Parent class file: gridbase.component.ts import { Component, OnInit } from '@angular/core'; ...

When trying to compile FirebaseUI with typescript and react-redux, users may encounter issues

I'm attempting to implement firebaseui for a login feature in react-redux using typescript. Here is the code snippet: import firebase from 'firebase'; import firebaseui from 'firebaseui'; import fire from '../FirebaseCreds&ap ...

Creating Dynamic Routes and Implementing Component Restrictions in Angular 2

Currently in the midst of designing an Angular 2 application, I find myself faced with some fundamental questions that could significantly impact the overall design. I'm struggling to determine the "right angular way" to address these concerns. Here a ...

Is it possible to access NgbdModalContent properties from a different component?

If I have a component with a template containing an Edit button. When the user clicks on it, I want to load another component as a dynamic modal template. The component is named ProfilePictureModalComponent and it includes the Edit button: (Angular code h ...

Troubleshooting the issue of Child redirect in Angular4 Routing not functioning

My Angular4 routing setup looks like this: {path: 'siteroot', component: SiteMessengerComponent}, { path: '', component: FrameDefaultComponent, children: [ { path: 'user/:userId', component: SiteUs ...

Utilizing a material design button with Angular 7 innerHTML

I am currently attempting to develop an Angular directive that enables password show/hide functionality. The show/hide feature is working properly, however, when trying to incorporate a material design (mat) button, it only displays the default HTML button ...

The issue arises when attempting to invoke a method from a global mixin in a Vue3 TypeScript component

I've been working on this challenge for the past week, and I would appreciate any help or insights from those who may have experience in this area. Currently, I am in the process of converting vue2-based code to vue3 for a new project. Instead of usi ...

Incorporating HTTP headers into Angular 6

Could someone confirm if this method is correct for adding headers to http requests in Angular 6? Upon inspecting the call through SwaggerUI, it appears that the required headers are: url -X GET --header 'Accept: application/json' --header &apo ...

"Looking to personalize marker clusters using ngx-leaflet.markercluster? Let's explore some ways to customize

I am currently struggling to implement custom cluster options in ngx-leaflet. My goal is simply to change all marker clusters to display the word "hello". The demo available at https://github.com/Asymmetrik/ngx-leaflet-markercluster/tree/master/src/demo/a ...

Encountering an Error in Angular Material 8 due to the Import of the Unforeseen Directive 'MatCard'

I keep encountering the error message displayed above in the console. I am currently working with Angular Material version 8.2.3. In my app.module.ts file, I have the following import statements related to Angular Material: import { MatInputModule, MatBu ...

Combining b2c and b2e integration through Azure Active Directory

Is there an efficient method for combining Azure AD b2c and b2e within an Angular application? Can we provide two separate buttons on the login page and redirect users based on their selection? Alternatively, could social login be utilized, keeping in mi ...

How can I retrieve the current table instance in NGX-Datatables?

I have a component that includes a table. This table receives RowData from another component through the use of @Input. How can I access the current instance of this table? Here is a snippet of my HTML: <ngx-datatable class="material" ...

Unable to retrieve this information within an anonymous function

I am currently working on converting the JSON data received from an API interface into separate arrays containing specific objects. The object type is specified as a variable in the interface. Here is the interface structure: export interface Interface{ ...

Dealing with redirecting authentication in Angular 2

We are working on a web application built with Angular 2 that interacts with a secure REST-API to retrieve data. When making the first request to the REST-API, it responds with a URL and a redirect(302) status code, prompting a GET request. However, Angul ...

The error message "Cannot send headers after they have already been sent to the client" is caused by attempting to set headers multiple

Although I'm not a backend developer, I do have experience with express and NodeJS. However, my current project involving MongoDB has hit a roadblock that I can't seem to resolve. Despite researching similar questions and answers, none of the sol ...

Linking custom Form Material Select component to FormControl validators

I have prepared an example on StackBlitz for reference. In my setup, there is a standard input form field along with a custom field displaying a select dropdown tied to an array. <form [formGroup]="formGroup"> <mat-form-field class="field"&g ...

Exploring Angular's @Input feature for components buried deep within the hierarchy

As a newcomer to Angular, I am diving into the best approach for addressing a specific scenario. On my page, I have multiple tiles resembling mat-cards, each equipped with various functionalities such as displaying charts, tables, and associated actions. ...

Ensuring a precise data type in a class or object using TypeScript

I am familiar with Record and Pick, but I struggle to grasp their proper usage. My goal is to ensure that a class or object contains specific data types such as strings, Booleans, arrays, etc., while also requiring properties or fields of Function type. in ...

Filter out all elements in an array except for one from a JSON response using Angular 2

After receiving a JSON response from a server via HTTP GET, the data structure looks like this: { searchType: "search_1", overview: [ "Bed", "BedSheets", "BedLinen", .. ...