A guide on setting a default constructor as a parameter in TypeScript

Through collaboration with a fellow coder on StackOverflow, I have mastered the art of specifying a constructor as an argument to a class:

type GenericConstructor<T> = { new(): T; }

class MyClass<T> {
  subclass: T;
  constructor(
    SubClass: GenericConstructor<T>
  ) {
    this.subclass = new SubClass();
  }
}

class MySubClass1 { a = "" }

class MySubClass2 { b = "" }

const withSubClass1 = new MyClass(MySubClass1);
const withSubClass2 = new MyClass(MySubClass2);

If feasible, I now aim to incorporate a default SubClass so that users of MyClass have the option to use the default functionality without specifying a subclass.

Below is the code in which I tried to implement this feature (unfortunately without success):

type GenericConstructor<T> = { new(): T; }

class DefaultSubClass { c = "" }

class MyClass<T> {
  subclass: T;
  constructor(
    SubClass: GenericConstructor<T> = DefaultSubClass // <== error!!
  ) {
    this.subclass = new SubClass();
  }
}

// …sliced out for brevity

Upon execution, TypeScript throws the following error at me:

Type 'typeof DefaultSubClass' is not assignable to type 'GenericConstructor<T>'.
  Type 'DefaultSubClass' is not assignable to type 'T'.
    'DefaultSubClass' can be assigned to the constraint of type 'T', however 'T' might be instantiated with a different subtype of constraint '{}'.

Grateful for all the advice and assistance provided as always.

Answer №1

The issue with this code is that it cannot guarantee type safety due to the ability to specify an explicit type argument. For example, consider someone calling your constructor like this:

let obj1: MyClass<MySubClass1> = new MyClass();
let obj2: MyClass<MySubClass2> = new MyClass();

In these cases, there will be no type errors because the type parameter T can be inferred as MySubClass1 and MySubClass2 respectively. However, neither object created will actually meet the specified type annotation requirements. Furthermore, even if you manage to make it work for one of them, it won't work for both since the type arguments in angle brackets are removed at compile-time, making it impossible to determine which constructor to use at runtime.

To address this issue, a safer approach would be to use a static factory method that is not generic. This allows the caller to create a class with the default subclass by using MyClass.create(), without the ability to demand a different type for T.

class MyClass<T> {
  static create(): MyClass<DefaultSubClass> {
    return new MyClass(DefaultSubClass);
  }

  subclass: T;
  constructor(
    SubClass: GenericConstructor<T>
  ) {
    this.subclass = new SubClass();
  }
}

Answer №2

If this behavior aligns with your requirements, there are two possible approaches that come to mind, each with its own set of drawbacks.


The first approach involves utilizing a generic parameter default for T:

class DefaultSubClass { c = "hey" }
class OtherSubClass { d = 123 };

class MyClass<T = DefaultSubClass> {
  subclass: T;
  constructor(
    SubClass?: new () => T
  );
  constructor(SubClass?: new () => any) {
    if (!SubClass) SubClass = DefaultSubClass;
    this.subclass = new SubClass();
  }
}

console.log(new MyClass().subclass.c.toUpperCase()); // HEY
console.log(new MyClass(OtherSubClass).subclass.d.toFixed(2)) // 123.00

This solution behaves as expected, but note the necessity to overload the constructor in order to address potential assignment issues between DefaultSubClass and T.

While relatively straightforward, it allows for the possibility of using the zero-argument constructor and manually specifying an incorrect T value:

console.log(new MyClass<OtherSubClass>().subclass.d.toFixed(2)); // oops

To prevent such unintended calls, another option is to weakly type the class constructor and then strengthen the typization by employing a type assertion to avoid compiler errors. This method is akin to scripting the constructor in pure JavaScript before enforcing TypeScript's strong typing as a library through declaration:

class _MyClass {
  subclass: any;
  constructor(ctor: new () => any = DefaultSubClass) {
    this.subclass = new ctor();
  }
}

The above represents the actual constructor function. The desired types can be specified as follows:

interface MyClass<T> {
  subclass: T;
}
interface MyClassCtor {
  new(): MyClass<DefaultSubClass>;
  new <T>(ctor: new () => T): MyClass<T>;
}

In this scenario, MyClassCtor consists of dual constructor signatures - one creating only MyClass<DefaultSubClass>, while the other generates MyClass<T> based on the provided T. The constructor value is then assigned to a variable conforming to the constructor type:

const MyClass = _MyClass as MyClassCtor;

Subsequently, the following code continues to achieve the intended functionality:

console.log(new MyClass().subclass.c.toUpperCase()); // HEY
console.log(new MyClass(OtherSubClass).subclass.d.toFixed(2)) // 123.00

Moreover, attempting the following operation will now result in an error:

new MyClass<OtherSubClass>(); // error! wrong number of arguments

Regrettably, these steps involve renaming MyClass temporarily and asserting a specific type due to the existing limitations surrounding constructor type parameters, which complicates direct implementation. Nonetheless, I hope one of these methods proves beneficial. Best of luck!


Click here for Playground link

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

Keep an ear out for updates on object changes in Angular

One of my challenges involves a form that directly updates an object in the following manner: component.html <input type="text" [(ngModel)]="user.name" /> <input type="text" [(ngModel)]="user.surname" /> <input type="text" [(ngModel)]="use ...

A step-by-step guide on leveraging swagger-autogen in TypeScript applications

Is it possible to integrate the swagger-autogen module into a Typescript project? I have attempted multiple methods, but have been unsuccessful. The error message "Failed" keeps appearing when using a swagger.js file: const swaggerAutogen = require("swagge ...

PageObjectModel Playwright, execute the locator().all() function - 'The execution context has been terminated, possibly due to navigating to another...'

Hey there, I'm currently working on a basic test using POM. Here is a snippet from one of my PageObjects Class: import { Expect, Page, Locator } from "@playwright/test"; export class InventoryPage { readonly page: Page; readonly addToC ...

How can I encode and decode a base64 string using AngularJS1 and TypeScript?

I am currently working with Angular1 using TypeScript and I have a question that needs some clarification. Within the environment that I am operating in, is there a method available to encode and decode a string in base64? Despite conducting extensive re ...

Setting up TypeScript in an Angular 2 project and integrating Facebook login

Currently, I am in the process of familiarizing myself with Angular 2 and typescript. Although things have been going smoothly so far, I have hit a roadblock while attempting to implement a Facebook login message. In my search for a solution, I stumbled up ...

Poorly packaged library - Custom Angular library - Node Package Manager

Recently, I've been delving into the process of publishing a simple Angular library on NPM. Despite following various tutorials (like those found here, here, and here), I faced difficulties when attempting to use it in a test project. MY JOURNEY In s ...

Instructions on transferring information from the app.component to its child components

I am currently working with Angular 6 and I have a specific requirement. I need to send data retrieved from an external API in my app.component to other child components. Instead of repeatedly calling the common API/service in every component, I want to ma ...

Validating nested objects in YUP with the potential for zero or multiple properties present

I am currently working on setting up yup validation for this object: placements: { 3: {}, 5: {}, 6: {0: 'D17'}, 7: {}, 8: {}, 9: {}, 10: {}, 11: {}, } The challenge I am facing is that an entry like 3: {} can be empty, and that's totally fi ...

Generating a unique serial ID using Angular/JS

I am in the process of developing a function that will create a unique serial id by replacing a string with the format; xxxx-xxxx-xxxx-xxxx. The desired outcome is a serial like this: ABCD-1234-EFGH-5678, where the first and third parts consist of letters ...

Utilizing lodash and Angular 8: Identifying an valid array index then verifying with an if statement

In my current project, I am developing an e-commerce angular application that includes a basket with two types of products: restaurant + show combos and gift cards. When a client reserves a restaurant, they must also reserve a show; conversely, the client ...

The type declaration for the Storage.prototype.setObject method

I'm facing a challenge in creating a d.ts file for the given DOM feature. Storage.prototype.setObject = function(key:string, value:any) { this.setItem(key, JSON.stringify(value)); } Storage.prototype.getObject = function(key:string) { var va ...

The TypeScript declarations for the scss module are malfunctioning

Just recently, I set up a React project using rollup. Below is the configuration file for my rollup setup: rollup.config.js import serve from "rollup-plugin-serve"; import livereload from "rollup-plugin-livereload"; import babel from &q ...

Gatsby website failing to create slugs as anticipated

While trying to follow the Gatsby tutorial, I ran into an issue with generating slugs for MDX files in a subdirectory of src/pages. For instance, if I have a file like src/pages/projects/devmarks/index.md, the expected slug according to the tutorial should ...

There was an issue thrown during the afterAll function: Unable to access properties of undefined

Recently, I upgraded my Angular project from version 15 to 15.1 and encountered an error while running tests. To replicate the issue, I created a new Angular 15.1 project using the CLI and generated a service with similar semantics to the one causing probl ...

Is there a way to achieve a seamless compilation in TypeScript?

Hopefully this is straightforward! TypeScript Latest version: 1.9.0-dev.20160512 (can be installed using npm install -g typescript@next as suggested by @basarat) Node v5.11.0 Windows 10.0.10586 First file: u1c.ts import * as u1u from "./u1u.ts" let p = ...

What is the best way to insert a placeholder React element into a different Component using TypeScript?

I've encountered a Typescript error that has me stumped. Check out the code snippet below: interface AppProps { Component: JSX.ElementClass; pageProps: JSX.ElementAttributesProperty; } const App = ({ Component, pageProps }: AppProps) => { co ...

Tips for saving a variable in Angular that is being received through a subscription as JSON:

Below is an example of the json I have: [{"id":"1","date":"2020-02-21","status":"present","studentid":"1"},{"id":"2","date":"2020-02-24","status":"present","studentid":"1"}] I am struggling to store the date in a variable using Angular when it is being s ...

Learn how to connect a formArray from the parent component to the child component in Angular with reactive forms, allowing you to easily modify the values within the formArray

In my parent component, there is a reactive form with controls and a form group. When the user selects a playerType from a dropdown menu, I dynamically add a formArray to the formGroup. This form array will contain either 2 or 3 form groups based on the p ...

Steps for incorporating a type declaration for an array of objects in a React application with TypeScript

How can I specify the type for an array of objects in React using TypeScript? Here is the code snippet: const SomeComponent = (item: string, children: any) => { //some logic } In this code, you can see that I am currently using 'any' as ...

Encountering an issue accessing a property retrieved from a fetch request in TypeScript

I am currently dealing with the property success defined in the API (reCAPTCHA). /** * The structure of response from the veirfy API is * { * "success": true|false, * "challenge_ts": timestamp, // timestamp of the challen ...