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

Tips for transferring data when clicking in Angular 5 from the parent component to the child component

I need assistance with passing data from a parent component to a child component in Angular 5. I want the child component to render as a separate page instead of within the parent's template. For example, let's say my child component is called & ...

Dynamic tag names can be utilized with ref in TypeScript

In my current setup, I have a component with a dynamic tag name that can either be div or fieldset, based on the value of the group prop returned from our useForm hook. const FormGroup = React.forwardRef< HTMLFieldSetElement | HTMLDivElement, React. ...

The issue with npm modules not appearing in EMCA2015 JS imports persists

I am currently in the process of developing a mobile application with Nativescript using the Microsoft Azure SDK. To get started, I installed the SDK via npm by running this command: $ npm install azure-mobile-apps-client --save However, upon attempting ...

Ways to Resolve the "TS2533: Object May Be Either 'Null' or 'Undefined'" Error on a Dynamic Object

I'm encountering an issue with the following code snippet: interface Schema$CommonEventObject { formInputs?: { [key: string]: Schema$Inputs; } | null; } interface Schema$Inputs { stringInputs?: Schema$StringInp ...

Having trouble implementing the Material UI time picker because it does not meet the required DateTime format

REVISE I recently switched my dataType from DateTime to TimeSpan in my code. I have a functioning MVC version that already uses TimeSpan, and the times are posted in HH:MM format. Now, I am unsure if the issue lies with the headers set up on Axios or if it ...

"Benefit from precise TypeScript error messages for maintaining a streamlined and organized architecture in your

As I dip my toes into the world of functional programming with typescript for a new project, I am encountering some challenges. In the provided code snippet, which follows a clean architecture model, TypeScript errors are popping up, but pinpointing their ...

React-snap causing trouble with Firebase

I'm having trouble loading items from firebase on my homepage and I keep running into an error. Does anyone have any advice on how to fix this? I've been following the instructions on https://github.com/stereobooster/react-snap and here is how ...

Bug in auto compilation in Typescript within the Visual Studios 2015

Currently, I am utilizing Visual Studio Pro 2015 with auto compile enabled on save feature. The issue arises in the compiled js file when an error occurs within the typescript __extends function. Specifically, it states 'Cannot read property prototyp ...

Using createStackNavigator along with createBottomTabNavigator in React Navigation version 5

I have recently started working with react native and I am using the latest version of react-navigation (v.5) in my react-native application. However, I encountered errors when trying to use createStackNavigator and createBottomTabNavigator together within ...

Why is the value always left unused?

I am having an issue with handling value changes on focus and blur events in my input field. Here is the code snippet: <input v-model="totalAmount" @focus="hideSymbol(totalAmount)" @blur="showSymbol(totalAmount)" /> ...

The parameter type 'typeof LogoAvatar' cannot be assigned to the argument type 'ComponentType<LogoProps & Partial<WithTheme>'

Why is the argument of type typeof LogoAvatar not assignable to a parameter of type ComponentType<LogoProps & Partial<WithTheme>? Here is the code snippet: import * as React from "react"; import { withStyles } from "@material-ui/core/style ...

Is there a counterpart to ES6 "Sets" in TypeScript?

I am looking to extract all the distinct properties from an array of objects. This can be done efficiently in ES6 using the spread operator along with the Set object, as shown below: var arr = [ {foo:1, bar:2}, {foo:2, bar:3}, {foo:3, bar:3} ] const un ...

Compilation of various Typescript files into a single, encapsulated JavaScript bundle

After researching countless similar inquiries on this topic, I have come to the realization that most of the answers available are outdated or rely on discontinued NPM packages. Additionally, many solutions are based on packages with unresolved bug reports ...

Unable to refresh the context following a successful API call

My current project in NextJS requires a simple login function, and I have been attempting to implement it using the Context API to store user data. However, I am facing an issue where the context is not updating properly after fetching data from the back-e ...

Tips for optimizing the performance of nested for loops

I wrote a for loop that iterates over 2 enums, sending them both to the server, receiving a value in return, and then calculating another value using a nested for loop. I believe there is room for improvement in this code snippet: const paths = []; for awa ...

Update gulp configuration to integrate TypeScript into the build process

In the process of updating the build system for my Angular 1.5.8 application to support Typescript development, I encountered some challenges. After a complex experience with Grunt, I simplified the build process to only use Gulp and Browserify to generat ...

Steps for creating a TypeScript project with React Native

Hey there, I'm just starting out with react-native and I want to work on a project using VS Code. I'm familiar with initializing a project using the command "react-native init ProjectName", but it seems to generate files with a .js extension inst ...

Error in TypeScript: The property 'data' is not found within type '{ children?: ReactNode; }'. (ts2339)

Question I am currently working on a project using BlitzJS. While fetching some data, I encountered a Typescript issue that says: Property 'data' does not exist on type '{ children?: ReactNode; }'.ts(2339) import { BlitzPage } from &q ...

In Angular, dynamically updating ApexCharts series daily for real-time data visualization

I am currently working with apexchart and struggling to figure out how to properly utilize the updateseries feature. I have attempted to directly input the values but facing difficulties. HTML <apx-chart [chart]="{ type: ...

What could be causing the rapid breakage of the socket in Ionic 3's Bluetooth Serial after just a short period

Although the code appears to be functioning correctly, it loses connection shortly after establishing it. This snippet contains the relevant code: import { Component } from '@angular/core'; import { Platform, NavController, ToastController, Ref ...