A class definition showcasing an abstract class with a restricted constructor access

Within my codebase, there is a simple function that checks if an object is an instance of a specific class. The function takes both the object and the class as arguments.

To better illustrate the issue, here is a simplified example without delving into the intricate details of the actual code:

function verifyType<T>(instance: unknown, classType:new () => T):instance is T {
    if (!(instance instanceof classType)) throw(`Expecting instance of ${classType.name}`);

    return true;
}

(class Foo { constructor() { } } const foo:unknown = new Foo(); verifyType(foo, Foo); // OK

However, issues arise when working with classes that have private constructors.

I comprehend the underlying logic behind these compiler errors. A private constructor implies that external code cannot construct the class, which makes perfect sense.

Despite this understanding, I am struggling to find an alternative approach that allows me to continue utilizing instanceof:

class Bar {
    private constructor() {
    }
    static create() {
        return new Bar();
    }
}
const bar:unknown = Bar.create();
verifyType(bar, Foo);
// OK
verifyType(bar, Bar);
// Argument of type 'typeof Bar' is not assignable to parameter of type 'new () => Bar'.
//  Cannot assign a 'private' constructor type to a 'public' constructor type.

Experimenting with T extends typeof Object

I came across a solution proposed in How to refer to a class with private constructor in a function with generic type parameters?

Based on the advice from the above link, I attempted the following:

function verifyType<T extends typeof Object>(instance: unknown, classType:T):instance is InstanceType<T>

Unfortunately, this approach raised errors due to the numerous static methods within Object:

const foo = new Foo();
verifyType(foo, Foo);
// Argument of type 'typeof Foo' is not assignable to parameter of type 'ObjectConstructor'.
//   Type 'typeof Foo' is missing the following properties from type 'ObjectConstructor': getPrototypeOf, getOwnPropertyDescriptor, getOwnPropertyNames, create, and 16 more.

Exploring Unconventional Solutions

I experimented with various iterations of typeof Object in an attempt to satisfy TypeScript while maintaining runtime accuracy, such as:

function verifyType<T extends Function & Pick<typeof Object, "prototype">>(instance: unknown, classType:T):instance is InstanceType<T>

Although this resolved the compile-time issues, it introduced runtime errors by allowing invalid types, which is unacceptable:

verifyType(bar, ()=>{});
// No compile-time errors
// Runtime Error: Function has non-object prototype 'undefined' in instanceof check 

Seeking Assistance from // @ts-expect-error

In conclusion, I might need to accept that this particular scenario is too specialized, and proper support may be lacking for the foreseeable future. As a result, I may need to provide exemptions in my code accordingly.

// @ts-expect-error Unfortunately, TS has no type representing a generic class with a private constructor
verifyType(bar, Bar);

Answer №1

My proposed method involves conducting inference on the prototype attribute of the classType parameter, as demonstrated below:

function validateType<T extends object>(
    instance: unknown,
    classType: Function & { prototype: T }
): asserts instance is T {
    if (!classType.prototype) throw ( 
      `Hold on, ${classType.name || "that"} is not a class constructor`);
    if (!(instance instanceof classType)) throw (
      `Expecting an instance of ${classType.name}`);
}

In TypeScript, you can use x instanceof y with y being a function type, hence the need for Function &. We utilize the generic type parameter T to represent the instance type of classType by linking it to the type of the prototype property.

Alongside Function & {protoype: T}, I've incorporated several changes from your original script:

  • The function validateType() now acts as an assertion function, returning asserts instance is T rather than functioning as a type guard function that returns instance is T. Assertion functions ensure narrowness without returning any value, unlike type guard functions which aid in control flow analysis.

  • I included an explicit check within the implementation to verify if classType.prototype exists. This addresses TypeScript's allocation of the prototype property for Function as the any type instead of the preferable unknown type, making it challenging to spot potential runtime errors related to non-constructors like ()=>{}. To tackle this limitation at the design level, the code attempts to fortify validateType() against such exceptions. You may consider incorporating additional logic like

    (!classType.prototype || typeof classType.prototype !== "object")
    based on your requirements.


Let's put it to the test:

class Foo {
    constructor() {
    }
    a = 1;
}
const foo: unknown = new Foo();
foo.a // error! Object is of type unknown
validateType(foo, Foo);
foo.a // acceptable
console.log(foo.a.toFixed(1)) // "1.0"

The compiler correctly recognizes that foo is a Foo post the invocation of validateType(foo, Foo).

class Bar {
    private constructor() {
    }
    static create() {
        return new Bar();
    }
    b = "z"
}
const bar: unknown = Bar.create();
validateType(bar, Bar);
console.log(bar.b.toUpperCase()); // "Z"

This scenario also validates well. The compiler approves verifyType(bar, Bar) due to Bar exhibiting a prototype property linked to type Bar, facilitating the subsequent access to the b property despite the presence of a private constructor within Bar.

Lastly:

const baz: unknown = new Date();
validateType(baz, () => { }); // 💥 Hold on, that is not a class constructor
baz // any

Although we couldn't catch validateType(baz, ()=>{}) during compilation, a meaningful runtime error ensues ensuring that the transition of baz from unknown to any doesn't negatively impact us.

Link to code on TypeScript Playground

Answer №2

To achieve this, you can modify the function to accept classes with a generate method as follows:

function validateClass<C extends (Function & { generate(...args: any[]): unknown }) | (new (...args: any[]) => unknown)>(
  object: unknown,
  classType: C
): object is C extends { generate(...args: any[]): infer T } ? T : C extends new (...args: any[]) => unknown ? InstanceType<C> : never {
  if (!(object instanceof classType)) throw `Expected object of type ${classType.name}`;

  return true;
}

While this approach does work, it may not be ideal if you have multiple classes with differently named generator methods, requiring you to update the function signature accordingly.

Interactive Demo


Additionally, for async factory methods, simply replace generate(): T with generate(): T | Promise<T>.

Answer №3

Fortunately, a clever solution is available

I encountered a similar issue some time back. The use of the built-in InstanceType<TClass> seems to be effective in resolving the instance type, but it comes with the constraint of requiring a public constructor:

type InstanceType<T extends abstract new (...args: any) => any> = ....

However, the limitation of needing a public constructor for InstanceType<TClass> is quite restrictive. It's suitable for obtaining runtime results, but inadequate for creating open generic types (TS Conditional Types tend to mix run-time and compile-time type results in a confusing manner!)

Thankfully, we can leverage how the compiler resolves types to find the best matching signature and overlook the accessibility modifier to allow private / protected constructors as follows:

type InstanceTypeSpy<TClass> = InstanceType<{ new(): never } & TClass>;

This method works because technically, new(): never does meet the expectations of InstanceType<>, even though the compiler chooses not to match against new(): never when possible to avoid resulting in never - thus providing us with the actual type (or defaulting to never if there truly isn't any constructor on TClass).

Fun Fact

To clarify the distinction between runtime and compile-time types, consider the recently introduced Awaited<T> - which gives us the runtime type when awaiting an instance of T. Now, imagine developing a generic library that needs to handle async / T = Promise<U> where the precise type T is unknown at compile time due to the unknown value of U.

This scenario represents an "open" generic type - once the value of U is known, it transforms into a "closed" generic type.

In such instances, we require the compile-time type; for example, if T = Promise<U>, then we need to access U.

The usage of Awaited<T> won't suffice since it may not align with U. If you attempt to assign a value of type U to something that expects Awaited<T>, a compile error will arise stating they are not assignable.

It's important to realize that await is recursive in nature. Consider a scenario where at runtime, U has a type of Promise<V>; this would result in our T being of type Promise<Promise<V>>. Upon awaiting an instance of type T, we receive a result of type V, significantly differing from a result of type Promise<V>. Thus, for an unknown T, the correctness of such library code cannot be guaranteed by the compiler.

For reference, below is an example of Awaited made non-recursive:

/**
 * Non-recursive version of Awaited<T>, offering the compile-time type necessary for generics.
 */
export type PromiseResolveType<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F): any } ?
    F extends ((value: infer V, ...args: any) => any) ? V :
    never :
  T;

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

Using TypeScript with knockout for custom binding efforts

I am in the process of developing a TypeScript class that will handle all bindings using Knockout's mechanisms. Although I have made progress with the initial steps, I have encountered a roadblock. While I can successfully bind data to my HTML element ...

I am facing an issue with the asynchronous function as it is displaying an error message

**I am facing an issue with displaying categories. I have attempted to do this using async function, however the data is not showing up** <div class="form-group"> <label for="category">Category</label> <select id="categor ...

The call in TypeScript React does not match any overload

Encountering an error with a React component that I wrote and seeking assistance. The primary component code snippet: export interface ICode { code: (code: string) => void; } export default class UserCode extends React.Component{ state = { formFil ...

Accessing video durations in Angular 2

Can anyone help me with retrieving the video duration from a list of videos displayed in a table? I attempted to access it using @ViewChildren and succeeded until encountering one obstacle. Although I was able to obtain the query list, when attempting to a ...

Angular time-based polling with conditions

My current situation involves polling a rest API every 1 second to get a result: interval(1000) .pipe( startWith(0), switchMap(() => this.itemService.getItems(shopId)) ) .subscribe(response => { console.log(r ...

A loop in JavaScript/TypeScript that runs precisely once every minute

Here is a snippet of my code: async run(minutesToRun: number): Promise<void> { await authenticate(); await this.stock.fillArray(); await subscribeToInstrument(this, this.orderBookId); await subscribeToOrderbook(this, this.orderBookId ...

Angular 2 routing for dynamic population in a grid system

My website is compiling correctly, however, in the Sprint dropdown menu where I have set up routing... <a *ngFor = "let item of sprint;" routerLink = "/Summary" routerLinkActive = "active"> <button *ngIf = "item.Name" mat-menu-item sty ...

Incorporating a TypeScript module into a JavaScript module within a React application

I'm encountering an issue with my React app that was created using create-react-app. I recently added a Typescript module to the project, which is necessary for functionality reasons. Although it will remain in Typescript, I made sure to install all t ...

Encountered an error while attempting to compare 'true' within the ngDoCheck() function in Angular2

Getting Started Greetings! I am a novice in the world of Angular2, Typescript, and StackOverflow.com. I am facing an issue that I hope you can assist me with. I have successfully created a collapse animation for a button using ngOnChanges() when the butto ...

"Implementing a Filter for Selecting Multiple Options in Ionic Framework

I need help with filtering books in an online library project using a modal page. The modal has 3 input fields for title, author, and year. How can I filter the books based on these inputs? Here is a snippet of my modal.html code: <ion-content pa ...

Refresh Material-Ui's Selection Options

Is there a way to properly re-render the <option> </option> inside a Material UI select component? My goal is to transfer data from one object array to another using the Material UI select feature. {transferData.map(data => ( <option ...

Angular 4 Web Application with Node-Red for Sending HTTP GET Requests

I am creating a unique service that utilizes Node-red to send emails only when a GET request is made to 127.0.0.1:1880/hello (node-red port), and an Angular 4 web app (127.0.0.1:3000) for client access. Upon accessing the /hello page from a browser, I rec ...

Resolve the error message "variable is utilized prior to assignment"

Looking at the code snippet below, import { STS } from 'aws-sdk' const sts = new STS({ region: 'us-east-1' }); let accessKeyId: string let secretAccessKey: string sts.assumeRole(params, function(err, data) { if (err) { ...

Learn how to display or conceal the HTML for 'Share this' buttons on specific routes defined in the index.html file

Currently, I am in the process of updating an existing Angular application. One of the requirements is to hide the "Share this buttons" on specific routes within the application. The "Share" module typically appears on the left side of the browser window a ...

Enhance the functionality of Immutable.js field by integrating a custom interface in Typescript

Imagine a scenario where the property name is field, essentially an immutable object. This means that methods like field.get('') and other immutable operations are available for use. Nevertheless, I have my own interface for this field which may ...

The PhpStorm code completion feature is not functioning properly when working with TypeScript code that is distributed through NPM

I am facing an issue with two NPM modules, which we will refer to as A and B. Both modules are written in TypeScript and compile into CommonJS Node-like modules. Module B has a dependency on module A, so I have installed it using the command npm install ...

Unit testing for Angular service involving a mock Http GET request is essential for ensuring the

I am seeking guidance on how to test my service function that involves http get and post calls. I attempted to configure the spec file by creating an instance of the service, and also consulted several sources on creating a mockhttp service. However, I enc ...

In Typescript 2.2, when using Express, the request and response objects are implicitly

I'm facing some challenges when it comes to incorporating types into my node/express project. Currently, I am using TypeScript 2.2 and express 4.x with the following npm installation for types: npm install --save-dev @types/express import * as expr ...

Error in Angular 2.0 final version: Unable to access the 'injector' property of null object

Upon transitioning from Angular 2 RC5 to 2.0 Final, I've encountered some errors while running my tests. It's puzzling me as to what could be causing this issue. TypeError: Cannot read property 'injector' of null at TestBed._create ...

Tips for adding items to a Form Array in Angular

I am working on a project with dynamic checkboxes that retrieve data from an API. Below is the HTML file: <form [formGroup]="form" (ngSubmit)="submit()"> <label formArrayName="summons" *ngFor="let order of form.controls.summons.controls; let i ...