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);
CodePudding user response:
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:
verifyType()
is now an assertion function returningasserts instance is T
instead of a type guard function returninginstance is T
. Type guard functions are supposed to returntrue
orfalse
and 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.prototype
is defined. TypeScript givesFunction
aprototype
property of theany
type instead of the saferunknown
type, 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:
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, () => { }); //