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

Encountering a 405 Error While Trying to Detect Location in Angular 7

I encountered an error 405 (Method Not Allowed) when trying to detect the location. Service public fetchWeatherDataByCoordinates(coordinates: ICoordinates): void { console.log("problem here") this.selectedLocationId.next(this.currentCoordinates ...

Tips for preventing the appearance of a horizontal scroll bar when utilizing the FlexLayoutModule in Angular

import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-property-feed', templateUrl: './property-feed.component.html', styleUrls: ['./property-feed.component.scss'] }) export class Prope ...

How can we retrieve URL parameters with the help of ActivatedRoute in Angular?

My current route configuration is as follows: { path: "events", component: SkeletonComponent, children: [ { path: ':id', component: ViewEventComponent } ] } The constructor of my component looks ...

The Angular2 component link imported in the core.module is not functioning properly

Adhering to the guidelines provided in the Angular style guide found at https://angular.io/styleguide#!#04-11, I have created a simple navigation setup. My app.component contains links like <a routerLink="hello">hello</a>, which work perfectly ...

Avoiding misreading email content with mail-listener2 - Tips for ensuring your function reads the correct emails

My E2E test utilizes the mail-listener2 to fetch emails. Though it generally works well, there is one recurring issue that I am struggling to resolve. Despite my extensive search for solutions online, the fix still eludes me. The problem arises when I use ...

Generating a custom type in Typescript based on specific array keys

Imagine I have a structure like this: export interface MyObject { id: number title: string } and then I create an array as shown below: const myArray: MyObject[] = [ { id: 2358, title: 'Item 1' }, { id: 85373, title: &a ...

A method for performing precise division on numbers in JavaScript, allowing for a specific precision level (such as 28 decimal places)

I've searched extensively for a library that can handle division operations with more than 19 decimal places, but to no avail. Despite trying popular libraries like exact-math, decimal.js, and bignumber.js, I have not found a solution. How would you ...

Leveraging TypeScript to Access Parameters in React Router

Currently, I am delving into the realm of TypeScript usage in my React projects and I have encountered a stumbling block when it comes to implementing React Router's useParams() feature. My import statement looks like this: import { useParams } from ...

Issue with locating module in Visual Studio 2015 when using Angular5 and TypeScript version TS2307

I'm currently attempting to integrate Angular in Visual Studio 2015 update 3, but encountering the following error: TS2307 cannot find module '../node_modules/@angular/core'. The error is shown in the image linked here. Can anyone help me fi ...

The Angular8 form encounters an error when trying to access the 'valid' property of an undefined value

I am implementing a register form to add an employee and including validation for accuracy. However, I encountered an error message that reads "cannot read property of 'valid' of undefined." Here is the code snippet: HTML <div class="conta ...

Steps to Transform String Array into Prisma Query Select Statement

I have a requirement to dynamically select Prisma columns based on client input: ['id', 'createdAt', 'updatedAt', 'Order.id', 'Order.Item.id', 'Order.Item.desc'] The desired format for selection ...

Tips for sorting an array with various data types in TypeScript while explicitly defining the type

I need help with a class that contains two fields, each being an array of different types but sharing the common field id. I am trying to create a method that filters the array and removes an item based on the provided id. enum ItemType { VEGETABLES = &a ...

An issue occurred with Ionic 4: TypeError - Unable to access property 'name' as it is undefined

None of the answers to similar questions have provided a solution for me SITUATION: I've been setting up a SQL Server connection from my Ionic app. You can check out my previous question for more details The workflow goes as follows: Ionic connects ...

In <R>, what does R represent when it is wrapped around an observer of type Observer<R>? Would it result in a Subscription, Function, or void?

The Angularfire2 project is in the process of adding a storage feature through a work-in-progress branch. This implementation includes two new files - an Observable class and a Factory function. Observable class export class FirebaseUploadTaskObservable& ...

What is the correct way to add type annotations to an Axios request?

I have meticulously added type annotations to all endpoints in my API using the openapi-typescript package. Now, I am looking to apply these annotations to my Axios requests as well. Here is a snippet of code from a Vue.js project I have been developing: ...

Angular 2 smart table row indexing

My Angular project is using ng2-smart-table and it's functioning correctly. However, I am looking for a way to display row numbers in each row without having to set a property in the data itself. If anyone has a workaround or solution, please let me k ...

Dealing with GraphQL mutation errors without relying on the Apollo onError() function

When managing access to an API call server-side, I am throwing a 403 Forbidden error. While trying to catch the GraphQL error for a mutation, I experimented with various methods. (Method #1 successfully catches errors for useQuery()) const [m, { error }] ...

Is there a way to prevent the Drop event in Angular2?

In my Angular2 application, I have created a directive for an input field. To prevent paste or Ctrl+V within the host element of this directive, I used the following code which is functioning flawlessly: @HostListener('paste', ['$event&apos ...

Communication breakdown between components in Angular is causing data to not be successfully transmitted

I've been attempting to transfer data between components using the @Input method. Strangely, there are no errors in the console, but the component I'm trying to pass the data from content to header, which is the Header component, isn't displ ...

Seeking assistance with TypeScript promises

Just starting out with typescript and nodejs, but I've got to tackle some issues in the typescript code. I'm looking to execute an ECS one-off task using Pulumi. I have the documentation on how to run the task from the taskDefinition, which can ...