Innovative techniques for class manipulation

Is there a way to implement type checking when extending a class with dynamic methods? For example, if you want to add methods to a class based on options provided to the constructor. This is a common scenario in plain JavaScript.

const defaults = {
  dynamicMethods: ['method1', 'method2'];
};

class Hello {
  constructor(options) {
    options.dynamicMethods.forEach(method => this[method] = this.common);
  }
  private common(...args: any[]) {
     // do something.
  }
}

const hello = new Hello(defaults);

The code above will work and allow you to call the dynamic methods, but you won't have intellisense support.

One way to address this issue is with the following approach:

class Hello<T> {
  constructor(options) {
    options.dynamicMethods.forEach(method => this[method] = this.common);
  }
  private common(...args: any[]) {
     // do something.
  }
}

interface IMethods {
  method1(...args: any[]);
  method2(...args: any[]);
}

function Factory<T>(options?): T & Hello<T> {
  const hello = new Hello<T>(options);
  return hello as T & Hello<T>;
}

To use this:

import { Factory } from './some/path'
const hello = new Factory<IMethods>(defaults);

This approach works, but it would be interesting to explore other possible solutions!

Answer №1

Exploring further with this concept led me to a solution that eliminates the need for defining a separate interface for each extension:

interface ClassOf<T> {
  new(...args: any[]): T
}

const extendClass = <T, S>(class_: ClassOf<T>, dynamicMethods: S) =>
  (...args: any[]) => {
    const newObj = new class_(args) as T & S;
    for (const key of Object.keys(dynamicMethods) as Array<keyof S>) {
      const method = dynamicMethods[key];
      (newObj as S)[key] = method; // omitting type signature
    }
  return newObj;
}

// demonstration:
class Greetings {
  constructor(public title) {}

  greeting() {
    return 'Greetings, ' + this.title;
  }
}

const extGreetConstructor = extendClass(Greetings, {phrase: (x: string) => x.toUpperCase(), display: (msg: string) => 'Message: ' + msg});
const extendedGreetings = extGreetConstructor('Alice');
const testA = extendedGreetings.phrase('hello');
const testB = extendedGreetings.display('world');
const testC = extendedGreetings.greeting();
console.log(testA, testB, testC);

interactive code snippet

With the exception of the constructor parameters, the inferred types are accurate. The functionality remains intact during execution. Returning an anonymous class is a possibility, although typing it can be cumbersome.

While this may not align precisely with your requirements, it could serve as a source of creative inspiration.

Answer №2

Here's a more efficient solution. Try using the Object.assign method:

class Example {
  constructor() {
    const dynamicProperty = "hello_world"

    Object.assign(this, {
      [dynamicProperty]: "new value"
    })
  }
}

console.log(new Example())

Result

Example { hello_world: 'new value' }

Answer №3

In this scenario, you can eliminate the necessity of the IMethods interface by implementing the Record type instead.

class Greetings {
  constructor(options: string[]) {
    options.forEach(m => this[m] = this.common);
  }
  private common(...args: any[]) {
     // perform actions here.
  }
}

function CreateGreetings<T extends string>(...options: T[]): Greetings & Record<T, (...args) => any[]> {
  const greetings = new Greetings(options);
  return greetings as Greetings & Record<T, (...args) => any[]>;
}

const greetings = CreateGreetings("action1", "action2");
greetings.action1();
greetings.action2();

Answer №4

With inspiration taken from @Oblosys's response, this method not only allows for static methods from the original class to be supported through inheritance but also eliminates the need for using any. Additionally, it returns a constructible class instead of a function, making it compatible with the use of new in TypeScript. This solution offers the flexibility to be continuously extended, enabling multiple plugins to enhance a logger while still allowing users to extend it further as needed.

// This type enables the extension of a given class with non-static methods
// Constructor arguments can be typed as an array, e.g., `[string]`
type ExtendedClass<Class, Methods, ArgsType extends unknown[] = []> = {
    new (...args: ArgsType): Class & Methods;
};

class DynamicallyExtendableClass {
    constructor(private name: string) {}

    static testMethod() {
        return "Blah";
    }

    dependent() {
        return `Hello ${this.name}!`;
    }

    static extend<Methods>(
        newMethods: Methods
    ): ExtendedClass<DynamicallyExtendableClass, Methods, [string]> & typeof DynamicallyExtendableClass {
        class Class extends this {
            constructor(name: string) {
                super(name);
                Object.assign(this, newMethods);
            }
        }
        return Class as ExtendedClass<Class, Methods, [string]> & typeof Class;
    }
}

// Extending with new methods
const Extended = DynamicallyExtendableClass.extend({
    method1: (num: number) => num,
    method2: (str: string) => str,
});

const ext = new Extended("Name");
const test1 = ext.method1(500);
const test2 = ext.method2("Test");

const test3 = Extended.testMethod();

const ExtExt = Extended.extend({
    blah: (str: string) => `Blah: ${str}`,
});

const test4 = new ExtExt("Name").blah("Test");
console.log(test1, test2, test3, test4, new ExtExt("Name").dependent());

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

``Are you experiencing trouble with form fields not being marked as dirty when submitting? This issue can be solved with React-H

Hey there, team! Our usual practice is to validate the input when a user touches it and display an error message. However, when the user clicks submit, all fields should be marked as dirty and any error messages should be visible. Unfortunately, this isn&a ...

Typescript: Securing Data with the Crypto Module

I am currently working on encrypting a password using the built-in crypto module. Previously, I used createCipher which is now deprecated. I am wondering if there is still an effective way to achieve this. Here is the old code snippet: hashPassword(pass: ...

React Typescript: The element is implicitly assigned an 'any' type as the type does not have an index signature

While attempting to locate a key of an object using an item from an array, I encountered an error... An Element implicitly has an 'any' type because type lacks an index signature I've replicated the issue in this sandbox https://codesandbo ...

Issue with NgModule in Angular application build

I'm facing an issue with my Angular application where the compiler is throwing errors during the build process. Here's a snippet of the error messages I'm encountering: ERROR in src/app/list-items/list-items.component.ts:9:14 - error NG6002 ...

Error: Unable to locate script.exe when spawning the Nodejs process

When trying to run an exe in my electron app, I am encountering an error. Even though the path is correct, it still throws an error. Uncaught Error: spawn exe/0c8c86d42f4a8d77842972cdde6eb634.exe ENOENT at Process.ChildProcess._handle.onexit (inter ...

Issue encountered in Angular app-routing module.ts: Type error TS2322: The type '"enabled"' cannot be assigned to type 'InitialNavigation | undefined'

When I recently updated my project from Angular 11 to 14, I encountered the following error when running "ng serve". Error: src/app/app-routing.module.ts:107:7 - error TS2322: Type '"enabled"' is not assignable to type 'InitialNavigation | u ...

Tips for building an effective delete function in Angular for eliminating a single record from a table

I've been working on creating a method to delete an employee by their ID, and I've tried various approaches in my VS Code. Unfortunately, all of them have resulted in errors except for this specific method. Whenever I click on the delete button, ...

Error: The type 'boolean | (() => void)' cannot be assigned to type 'MouseEventHandler<HTMLButtonElement> | undefined'

Playing audio in a NextJS app while writing code in TypeScript has been an interesting challenge. The onClick() function performs well in the development environment, triggered by npm run dev. <button onClick ={toggle}> {playing ? "Pause" : ...

How do I inform Jest that spaces should be recognized as spaces?

Here is some code snippet for you to ponder: import { getLocale } from './locale'; export const euro = (priceData: number): string => { const priceFormatter = new Intl.NumberFormat(getLocale(), { style: 'currency', currenc ...

Material-UI - TypeScript - Autocomplete utilizing getOptionLabel and renderOption for optimized selection functionality

I am attempting to showcase member and company using MUI Autocomplete. I have an array called suggestion that contains the options to display. [ { "__typename": "Member", "id": "ckwa91sfy0sd241b4l8rek ...

Generate user-customized UI components from uploaded templates in real-time

Summary: Seeking a solution to dynamically generate UI pages using user-provided templates that can be utilized for both front-end and back-end development across various use cases. Ensuring the summary is at the top, I am uncertain if this question has b ...

Issue: The formGroup function requires a valid FormGroup instance to be passed in. Data retrieval unsuccessful

My goal is to retrieve user data from a user method service in order to enable users to update their personal information, but I'm encountering an error. Currently, I can only access the "prenom" field, even though all the data is available as seen in ...

Retrieve data from a URL using Angular 6's HTTP POST method

UPDATE: Replaced res.json(data) with res.send(data) I am currently working on a web application using Angular 6 and NodeJS. My goal is to download a file through an HTTP POST request. The process involves sending a request to the server by calling a func ...

Uploading multiple files simultaneously in React

I am facing an issue with my React app where I am trying to upload multiple images using the provided code. The problem arises when console.log(e) displays a Progress Event object with all its values, but my state remains at default values of null, 0, and ...

Having trouble with data types error in TypeScript while using Next.js?

I am encountering an issue with identifying the data type of the URL that I will be fetching from a REST API. To address this, I have developed a custom hook for usability in my project where I am using Next.js along with TypeScript. Below is the code sni ...

Developing applications using ReactJS with Typescript can sometimes lead to errors, such as the "onclick does not exist on type x

In the code snippet below, I have a method that renders a delete icon and is used in my main container. Everything functions correctly except for a small cosmetic issue related to the type any that I am struggling to identify. import React from 'reac ...

Creating a structured state declaration in NGXS for optimal organization

Currently, I'm delving into the world of NGXS alongside the Emitters plugin within Angular, and I find myself struggling to grasp how to organize my state files effectively. So far, I've managed to define a basic .state file in the following man ...

I keep encountering the issue where nothing seems to be accessible

I encountered an error while working on a project using React and Typescript. The error message reads: "export 'useTableProps' (reexported as 'useTableProps') was not found in './useTable' (possible exports: useTable)". It ...

Transferring an event to a component nested two levels deep

Within my Angular 2 ngrx application, I am working with a structure that involves nested elements: parentContainer.ts @Component({ template: `<parent-component (onEvent)="onEvent($event)" ></parent-component>`, }) class ParentContaine ...

Remix is throwing a Hydration Error while trying to process data mapping

While working on my Remix project locally, I encountered a React hydration error. One thing I noticed is that the HTML rendered by the server does not match the HTML generated by the client. This issue seems to be related to the Material UI library usage. ...