Creating child class instances inside parent classes in TypeScript

Could you please review the following example:

type Task = (input: number) => number;

abstract class ParentClass {
    constructor(protected tasks: Task[] = []) {}

    protected abstract getDescendantClass<T extends this>(): new (tasks: Task[]) => T;

    protected addTask(task: Task) {
        const DecendantClass = this.getDescendantClass();
        return new DecendantClass([...this.tasks, task]);
    }

    exec(value: number) {
        for (const task of this.tasks) {
            value = task(value);
        }
        return value;
    }
}

abstract class ChildClass extends ParentClass {
    add(toAdd: number) {
        return this.addTask((v) => v + toAdd);
    }
}

class FinalChildClass extends ChildClass {
    // ❌ TS error (1)
    protected getDescendantClass() {
        return FinalChildClass;
    }

    multiply(factor: number) {
        return this.addTask((v) => v * factor);
    }
}

const Tasker = new FinalChildClass();

// ✔️ No TS errors (2) - I'm able to chain functions from all children
const task = Tasker.add(2).multiply(2).add(2);
const result = task.exec(2);

console.log(result); // 10

The goal is to develop a class that can generate instances of descendant classes dynamically.

I have successfully implemented it and created a chaining API as desired (2), but I encountered a TypeScript error (1):

'FinalChildClass' is assigned to the constraint of type 'T',
however, 'T' could potentially be instantiated with a different subtype of constraint 'FinalChildClass'.

What typing adjustments can be made to resolve this issue?

Answer №1

There are several valid justifications for this specific type error. Given your current usage of these features, your approach appears quite logical. However, when we expand on the example slightly, you may start to grasp why it falls apart.

Let's establish a new class called "FinalFinal", which extends "Final". It's important to note that this is completely permissible.

class FinalFinalChildClass extends FinalChildClass {
    divide(divisor: number) {
        return this.addTask((v) => v / divisor);
    }
}

Our updated code looks like this:

const Tasker = new FinalFinalChildClass();

// ✔️ No TS errors (2) - I'm able to chain functions from all children
const task = Tasker.add(2).multiply(2).add(2).divide(5);
const result = task.exec(2);

console.log(result); // 2

TypeScript approves without any issues (except for the noticeable error you've already pointed out), but during runtime our getDescendantClass() fails to deliver a constructor that would build a new FinalFinalChildClass as promised. Instead, it produces a FinalChildClass. Thus, although TypeScript compiles successfully, we encounter the following runtime error:

Tasker.add(...).multiply(...).add(...).divide is not a function 

This error message is accurate: "T was instantiated with a different subtype of constraint 'FinalChildClass'". As a result, there is now a disconnect between our TypeScript types and the actual prototype chain at runtime.

Try it out yourself.

We require a more robust approach that can construct new instances of Descendant classes successfully.

One solution is to abandon the flawed idea of determining the descendant type within the base class. This information is unknown and unknowable. Instead, we can rely on convention and utilize existing JavaScript APIs to retrieve the constructor for the current this.

Our revised strategy eliminates the faulty getDescendantClass() function in favor of using

Object.getPrototypeOf(this).constructor
to obtain the constructor. (Note that the returned type is any, so we need to provide explicit typing to specify the expected return type to the compiler.)

abstract class ParentClass {
    constructor(protected tasks: Task[] = []) {}

    protected addTask(task: Task) {
        const ctor:new (tasks: Task[]) => this = Object.getPrototypeOf(this).constructor;
        return new ctor([...this.tasks, task]);
    }

    exec(value: number) {
        for (const task of this.tasks) {
            value = task(value);
        }
        return value;
    }
}

abstract class ChildClass extends ParentClass {
    add(toAdd: number) {
        return this.addTask((v) => v + toAdd);
    }
}

class FinalChildClass extends ChildClass {
    multiply(factor: number) {
        return this.addTask((v) => v * factor);
    }
}

class FinalFinalChildClass extends FinalChildClass {
    divide(divisor: number) {
        return this.addTask((v) => v / divisor);
    }
}

const Tasker = new FinalFinalChildClass();

// ✔️ No TS errors (2) - I'm able to chain functions from all children
const task = Tasker.add(2).multiply(2).add(2).divide(5);
const result = task.exec(2);

console.log(result); // 2

This method provides strong typing during compilation:

And ensures error-free functionality at runtime:

While there remains a possibility that someone could create a class extending ParentClass with an incompatible constructor implementation, no TypeScript feature exists to restrict how subclasses are designed. Some level of adherence to conventions and proper documentation will be necessary, but this practice isn't uncommon in our field.

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

Setting up TypeScript in Node.js

A snippet of the error encountered in the node.js command prompt is as follows: C:\Windows\System32>npm i -g typescript npm ERR! code UNABLE_TO_VERIFY_LEAF_SIGNATURE npm ERR! errno UNABLE_TO_VERIFY_LEAF_SIGNATURE npm ERR! request to https:/ ...

Can the arrow function properly subscribe to an Observable in Angular and what is the accurate way to interpret it?

I'm currently working through the official Angular tutorial: https://angular.io/tutorial/toh-pt4 Within this tutorial, there is a component class that subscribes to a service: import { Component, OnInit } from '@angular/core'; import { He ...

What purpose does the question mark (?) serve after a class name when invoking class methods in TypeScript?

One interesting element within my TypeScript code snippet is the presence of the statement row?.delete();. I'm curious about the significance of the question mark in this context. What would be the outcome if 'row' happened to be null? Ap ...

Exploring an array in React using typescript

I have a persistent data structure that I'm serving from the API route of my Next.js project. It consists of an array of objects with the following properties: export interface Case { id: string; title: string; participants: string[]; courtDat ...

Utilizing a d.ts Typescript Definition file for enhanced javascript intellisene within projects not using Typescript

I am currently working on a TypeScript project where I have set "declaration": true in tsconfig.json to generate a d.ts file. The generated d.ts file looks like this: /// <reference types="jquery" /> declare class KatApp implements IKatApp ...

The cursor in the Monaco editor from @monaco-editor/react is not aligning with the correct position

One issue I am facing with my Monaco editor is that the cursor is always placed before the current character rather than after it. For example, when typing a word like "policy", the cursor should be placed after the last character "y" but instead, it&apos ...

Encountering an issue while trying to utilize Vuex in Vue with TypeScript

I recently utilized npm to install both vue (2.4.2) and vuex (2.3.1). However, when attempting to compile the following code snippet, I encountered the following error: https://i.stack.imgur.com/0ZKgE.png Store.ts import Vue from 'vue'; import ...

What could be causing the error in Angular Universal Server Side Rendering after deployment on Firebase Hosting?

Currently immersed in Angular development utilizing third-party libraries such as Angular CLI/Angular Universal, following the guidelines laid out here. Also, integrating Firebase hosting and real-time database. The application works flawlessly on my local ...

Setting the default selected row to the first row in ag-Grid (Angular)

Is there a way to automatically select the first row in ag-grid when using it with Angular? If you're curious, here's some code that may help: https://stackblitz.com/edit/ag-grid-angular-hello-world-xabqct?file=src/app/app.component.ts I'm ...

class that receives function yields

My main code works perfectly fine except for the final print operation! When the code runs, it repeats and starts receiving inputs again. If you have any suggestions on how to pass the results of these functions to another one, I would greatly appreciate ...

What causes the error message saying 'undefined' cannot be assigned to the specified type ...?

I just finished developing an innovative Angular application. Within the app.component.html file, I have included: <bryntum-scheduler #scheduler [resources] = "resources" [events] = "events" [columns] = "schedul ...

Converting interfaces into mapped types in TypeScript: A guidance

Understanding Mapped Types in TypeScript Exploring the concept of mapped types in TypeScript can be quite enlightening. The TypeScript documentation provides a neat example to get you started: type Proxy<T> = { get(): T; set(value: T): void ...

Angular 6 - Using properties in classes

Considering a component structured as follows: import { Component, OnInit, ViewChild } from '@angular/core'; @Component({ selector: '...', templateUrl: './...html', styleUrls: ['./...scss'] }) export class Te ...

Having trouble declaring custom pipes in Angular

A new pipe named 'shortend.pipe.ts' has been created within the app folder. import { PipeTransform } from "@angular/core"; export class ShortendPipe implements PipeTransform { transform(value: any, ...args: any[]) { return ...

Is duplication of values causing issues when creating classes within dictionaries?

I have been working on storing lists into a list associated with a specific "person". I attempted to achieve this using classes as shown below: class data(): # Contains list of x, y, z, time lists x = [] y = [] z = [] time = [] class ...

Is there a way to establish a pre-defined key in a mat-table?

I am fetching data from an API and I need to display it in a key-value format on a mat table. The keys are predefined and not provided by the API. The data retrieved from the API is shown here: image1 I want to display it on a mat table like this: mat ta ...

Incorporating a class element within an Angular 2 directive

When working with Angular 2 directives, one way to add an element is by using the following code: this._renderer.createElement(this._el.nativeElement.parentNode, 'div'); After adding the element, how can I set its class and keep a reference to ...

Interfaces are limited to extending an object type or a combination of object types that have statically defined members

Having trouble utilizing TextFieldProps in my code. Any tips on how to effectively use TextFieldProps? Any help is appreciated. https://i.sstatic.net/mzTfe.png import TextField, { TextFieldProps } from '@mui/material/TextField'; import { colorTh ...

What is the appropriate way to specify the type of a function parameter to be an express app?

I have a node server running on express and I am looking to add metrics to it during the setup process. Here is a snippet of my code: const app = express(); installMetrics(app); While TypeScript can accurately determine the type of app since I have insta ...

What could be causing the "ERROR TypeError: Cannot read property 'length' of undefined" message to occur with a defined array in my code?

Even though I defined and initialized my array twice, I am encountering a runtime error: "ERROR TypeError: Cannot read property 'length' of undefined." I have double-checked the definition of the array in my code, but Angular seems to be playing ...