Creating an abstract static factory with a protected constructor in Typescript

I've been working on this code snippet:

import { IValueObject } from "../../shared/domain/IValueObject";
import { AbstractNanoidGenerator } from "../../shared/infrastructure/AbstractNanoidGenerator";

export class CompanyID extends AbstractNanoidGenerator implements IValueObject<string, CompanyID> {
  protected constructor(private companyID: string) {
    super();
  }

  get value(): string {
    return this.companyID;
  }

  equals(vo: CompanyID) {
    return vo.companyID === this.companyID;
  }
}

export abstract class AbstractNanoidGenerator {
  static generate<T extends AbstractNanoidGenerator>(this: new (ID: string) => T): T {
    return new this(nanoid());
  }

  static fromString<T extends AbstractNanoidGenerator>(this: new (ID: string) => T, id: string) {
    return new this(id);
  }
}

export interface IValueObject<T, FinalClass> {
  readonly value: T;
  equals(_vo: FinalClass): boolean;
}

It's functioning properly, but there's a request to declare the child class constructor as public, which I am hesitant to do.

The 'this' context of type 'typeof CompanyID' is not assignable to method's 'this' of type 'new (ID: string) => CompanyID'. Cannot assign a 'protected' constructor type to a 'public' constructor type.ts(2684)

Do you have any suggestions or ideas on how to tackle this issue?

You can check out the code playground here

Best regards

Answer №1

The Issue

protected serves as an access modifier and cannot be applied to a type or an interface since they represent public contracts. Therefore, it is not possible to define protected properties for a type intended for function parameters, including this.

In the scenario of this: new (ID: string) => T, it specifies that the this argument must be of type new (ID: string) => T, which exclusively allows access to public properties.

As of November 2021 (TS 4.5), TypeScript still lacks the capability to reference protected properties within an interface or to define protected constructors within a class (source). There is hope for future support concerning protected constructors with the introduction of abstract Construct Signatures in TS 4.2 (details).

The Workaround

Understanding that enforcing a protected constructor for the passed this parameter is unattainable, what superior alternative exists beyond any? :)

Consider assuming that any subclass of AbstractNanoidGenerator invariably possesses a protected constructor which accepts an (ID: string) argument and returns an instance of itself (similar subclass).

Consequently, our objective shifts to mandating that the this parameter is "a subclass of AbstractNanoidGenerator" and accurately specifying the return type as an instance of that subtype.

Class vs. Instance Object

Let's differentiate between a "Class" and "Instance Object".

When declaring let id: CompanyID, the type of id represents an instance of the CompanyID class.

However, the this in the static functions above should not be equated to this: CompanyID, which would imply a need for an instance. Rather, we require the actual CompanyID class itself to be passed/used, characterized by the type typeof CompanyID (refer to this explanation). :)

How to Determine the Class Itself?

In TypeScript, there are two primary methods to specify a type as a "Class" for enforcing the this parameter in our context.

Class Type Approach 1: Leveraging new

A prevalent approach to indicating a "Class" involves verifying that invoking new on the type results in an "Instance". This concept inspired (these solutions) and led to the following utility functions.

export interface Type<T> extends Function {
    new (...args: any[]): T;
}

// --- Alternatively ---
export type Constructor<VALUE_T = any> = new (...args: any[]) => VALUE_T;

Class Type Approach 2: Utilizing prototype

The prior version may not function for classes with protected constructors like our CompanyID. Such classes lack a public constructor to be captured by new, resulting in failure.

Hence, the alternative relies on the existence of a property named prototype.

export type ClassDefinitionFor<T> = { prototype: T };

(Therefore,

let theClass:ClassDefinitionFor<CompanyID> = CompanyID
functions as intended!)

Conversely...

type InstanceOfClass<T> = T extends { prototype: infer R } ? R : never;

Refining the Parameters

Applying the above utility functions, the subsequent code is formulated.

By using the as unknown as workaround, TypeScript recognizes that the this object is a Class object extending AbstractNanoidGenerator, possessing a constructor that produces an object with the type defined on the prototype of that class.

type ClassDefinitionFor<T>  = { prototype: T };
type InstanceOfClass<T>     = T extends { prototype: infer R } ? R : never;

export abstract class AbstractNanoidGenerator {
  static generate<
    INSTANCE_T extends AbstractNanoidGenerator,
    CLASS_T extends ClassDefinitionFor<INSTANCE_T>,
    CTR_T extends new (ID: string) => InstanceOfClass<CLASS_T>,
  >(this: CLASS_T): InstanceOfClass<CLASS_T> {
    return new (this as unknown as CTR_T)(Math.random().toString());
  }
}

(Access the complete TS Playground with supplementary tests.)

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

Issue with Webpack throwing 'window undefined' persists despite using the 'use client' configuration in React/Next.js

I've been using Typescript 5, React 18, and Next.js 14 as my tech stack, and I keep encountering similar errors with various libraries. One of the errors I often face is ReferenceError: window is not defined. This error originates from a third-party ...

The current version of Firebase functions is not reflecting the most recent modifications when running "firebase serve"

Exploring firebase functions has been a fun journey for me. Everything works smoothly when I deploy to the firebase server using the command firebase deploy --only functions. However, I wanted to test my functions locally before deploying them, and encount ...

Unable to retrieve a substring value in Angular using Typescript

html <p> <input type="text" maxlength="40" (input)="recipientReference = deleteSpacing(recipientReference)" [(ngModel)]="recipientReference" style="width: 30vw; padding: 5px;border: 1px solid;border ...

A guide on simulating x-date-pickers from mui using jest

I have successfully integrated a DateTimePicker into my application, but I am facing an issue with mocking it in my Jest tests. Whenever I try to mock the picker, I encounter the following error: Test suite failed to run TypeError: (0 , _material.gen ...

Error in Angular compiler-cli: The namespace 'ts' does not contain the exported member 'ResolutionMode'

Currently working on a web application using Angular 16 in Webstorm. The application is still in the pre-release stage, with only minimal functionality completed so far. While editing with ng serve running to test changes as they were made, encountered an ...

Are there alternative methods for incorporating react-multi-carousel in a project utilizing NEXT.js and React/Typescript?

I installed the react-multi-carousel module and configured a simple carousel, but it's not functioning properly. The issue seems to be related to the absence of <li> tags. Here is the HTML that was rendered: <ul class="react-multi-caro ...

Setting cursor position in input field when navigating with arrow keys: What are the ways to achieve accessibility?

I have implemented arrow key navigation in a table, allowing navigation with the up, down, left, and right arrow keys. How can I ensure that the cursor always stays on the right side of the input field during navigation? Check out my Stackblitz example he ...

When attempting to retrieve the first element of an array using TypeScript, the browser unfortunately returned an error stating that the property was undefined

After creating a method called getFooterContent to retrieve data and display it using the method, I encountered the following error: platform-browser.umd.js:1900 ERROR: TypeError: Cannot read property 'content' of undefined getFooterConten ...

Error: Trying to access 'items' property of an undefined variable leads to a TypeError

I am currently working on creating a webpage with Angular 8 that fetches data from a REST API in JSON format by passing the request ID. My goal is to display this data in an HTML table that I have created. However, I encountered the following error: Typ ...

Exploring the world of TypeScript type mappings

I'm currently working on enhancing a function with type annotations. This particular function takes an array of typed objects as parameters and returns a mapped array of a different type: const createAnimals = <T extends AnimalFactory<any>[]& ...

Encountering a Static Injector error in Angular 5 while trying to inject a component from a shared module

Here lies the code for my component file. My goal is to develop a reusable modal component using ng-bootstrap's modal feature. However, upon trying to import the following component from the shared module, I encounter a static injector error. Despite ...

What steps can I take to make sure that the asynchronous initialization in the Angular service constructor has finished before proceeding?

Hello experts, can you advise on ensuring that asynchronous initialization in the service constructor is completed before calling other functions within the class? constructor() { var sock = new SockJS(this._chatUrl); this.stompClient = Stomp.ov ...

Tips for utilizing mergeWith with Subjects in an Angular application

Objective: Creating a Combined Filter with 3 Inputs to refine a list Conditions: Users are not required to complete all filters before submission The first submit occurs when the user inputs data Inputs are combined after more than one is provided Appro ...

The code within a for loop may not function properly when placed within the ngOnInt() function

I am struggling with running a for loop inside ngOnInit in Angular. I have a service that has a method getAllNews() which returns an array, not an observable. Since I can't use the subscribe() method, I want to iterate over this array. When I console ...

Attempting to locate a method to update information post-editing or deletion in angular

Are there any methods similar to notifyDataSetChange() in Android Studio, or functions with similar capabilities? ...

Strategies for persisting data in React using local storage to ensure information is retained after page refresh

I am attempting to store searched recipes data in local storage so that when the user refreshes, the data from the last searched recipe will still be available. I have tried saving the recipes data to local storage when a new search request is made and se ...

Is it possible for transclusion to display content from external sources using *ngIf and <ng-content>?

In my Angular4 Project, I have come across this snippet of code: <div class="divider"></div> <ng-content select=".nav-toggle"></ng-content> Now, I am trying to figure out a way to display the divider only when there is content pr ...

Determining the length of an array of objects nested within another object

I received a response from the API called res. The response is in the following format: {"plan":[{"name":"ABC"},{"name":"DEF"}]}. I am attempting to save this response in my TypeScript code as shown below: ...

The juvenile entity does not align with the foundational entity [typescript]

After setting "strict": true in my tsconfig.json, I encountered compiler errors when attempting to run the code. To replicate and explore this issue further, you can try running the following code snippet. The problem arises when the child clas ...

Using mergeMap in conjunction with retryWhen allows for the resumption of retries from the exact point of failure, without needing

I have a list of URLs, the number of which is unknown until it stops (depending on some condition). This is how I am currently using them: from(observableUrls) .pipe( mergeMap(url => callHttpService(url) , 4), retryWhen( // Looking f ...