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;
}
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);
My suggested approach is to do inference on the
prototypeproperty of theclassTypeparameter, like this:Generally speaking TypeScript will let you write
x instanceof yifyis of some function type, so henceFunction &, and then we infer the generic type parameterTcorresponding to the instance type ofclassTypeby saying that it's the type of theprototypeproperty (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:verifyType()is now an assertion function returningasserts instance is Tinstead of a type guard function returninginstance is T. Type guard functions are supposed to returntrueorfalseand the compiler uses this in its control flow analysis. Assertion functions on the other hand don't return any value, they just ensure that the narrowable thing is definitely narrowed. If you have a type guard function that always returnstrue(and throws otherwise) then you probably want an assertion function instead.I've done an explicit check inside the body to see if
classType.prototypeis defined. TypeScript givesFunctionaprototypeproperty of theanytype instead of the saferunknowntype, and this unfortunately makes it almost impossible to detect that a non-constructor like()=>{}would throw a runtime error if made the target ofinstanceof. Instead of trying to represent that in the type system, then, I just try to hardenverifyType()'s implementation against such edge cases. You might want to check(!classType.prototype || typeof classType.prototype !== "object")or any other logic you feel is necessary.Let's test it:
Looks good, the compiler sees that
foois aFooafter (but not before)verifyType(foo, Foo)has been called.Also looks good. The compiler is happy to allow
verifyType(bar, Bar)becauseBarhas aprototypeproperty of typeBar(or so it appears to the compiler) and thus you can then access thebproperty ofBar. It doesn't matter thatBarhas aprivateconstructor.And finally:
We weren't able to flag
verifyType(baz, ()=>{})at compile time, but at least you get a meaningful runtime error, so the subsequent "narrowing" ofbazfromunknowntoany(ugh, thanksany) doesn't affect us.Playground link to code