typescriptprivate-constructor

Type representing a generic class with a private constructor


I have a simple function that asserts that an object is an instance of a class. The object and the class are both passed as arguments to the function.

The following is a simplified example that illustrates the issue (without getting into the nitty-gritty of the real code):

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

    return true;
}

(TypeScript Playground link)

This function works well with most classes:

class Foo {
    constructor() {
    }
}

const foo:unknown = new Foo();
verifyType(foo, Foo);
// OK

However, I get compiler errors if the class has a private constructor.

I understand the reasons for this and it makes sense. A constructor type implies that the calling code could construct the class and a private constructor means this is not permitted.

However, I can't find an alternative that still allows me to use 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.

Trying T extends typeof Object

I read How to refer to a class with private constructor in a function with generic type parameters?

Based on the answer to that question, I thought that maybe I could use the following:

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

However, this appears to throw errors because of Object's many static methods:

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.

Getting exotic

I tried many variations on typeof Object to see if I could both satisfy TypeScript and also ensure runtime correctness, including:

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

However, while this solved the compile-time errors, it appears to have done so by allowing types that fail at runtime, which is not acceptable:

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

Help me // @ts-expect-error, you're my only hope

In the end, this might be too niche a use-case and I might have to accept that this edge-case won't be supported any time soon and exempt my code accordingly.

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

Solution

  • My suggested approach is to do inference on the prototype property of the classType parameter, like this:

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

    Generally speaking TypeScript will let you write x instanceof y if y is of some function type, so hence Function &, and then we infer the generic type parameter T corresponding to the instance type of classType by saying that it's the type of the prototype property (TypeScript models class constructor prototypes as being the same type as an instance of the class, even though in practice that is not usually true. See microsoft/TypeScript#44181 and probably others for more information).

    In addition to Function & {protoype: T} I've made a few changes from your code:


    Let's test it:

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

    Looks good, the compiler sees that foo is a Foo after (but not before) verifyType(foo, Foo) has been called.

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

    Also looks good. The compiler is happy to allow verifyType(bar, Bar) because Bar has a prototype property of type Bar (or so it appears to the compiler) and thus you can then access the b property of Bar. It doesn't matter that Bar has a private constructor.

    And finally:

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

    We weren't able to flag verifyType(baz, ()=>{}) at compile time, but at least you get a meaningful runtime error, so the subsequent "narrowing" of baz from unknown to any (ugh, thanks any) doesn't affect us.

    Playground link to code