I am trying to write two jest functions that expect an object to be an instance of a certain type or not.
The positive example expectInstanceOf works like a charm, but the negative one expectNotInstanceOf doesn't.
export function expectInstanceOf<E, A extends unknown[]>(obj: unknown, type: new (...args: A) => E): asserts obj is E {
expect(obj).toBeInstanceOf(type);
}
export function expectNotInstanceOf<E, A extends unknown[]>(obj: unknown, type: new (...args: A) => E): asserts obj is Exclude<typeof obj, E> {
expect(obj).not.toBeInstanceOf(type);
}
class Foo {
foo() {
/**/
}
}
class Bar {
bar() {
/**/
}
}
function foo(obj: Foo | Bar) {
expectInstanceOf(obj, Foo);
obj.foo();
}
function notFoo(obj: Foo | Bar) {
expectNotInstanceOf(obj, Foo);
obj.bar(); // Property 'bar' does not exist on type 'Foo | Bar'.
}
How can I fix expectNotInstanceOf?
CodePudding user response:
In your code, typeof obj is always the unknown type (you can't use the typeof type query operator to "abstract" a specific type), and the Exclude<T, U> utility type only filters union types in T. unknown is not a union, and so Exclude<unknown, E> is very likely going to be unknown (I suppose it could also be the never type if E is unknown). So while expectInstanceOf narrows to E, expectNotInstanceOfnarrows tounknown`, which isn't helpful.
What you want to do is make obj have its own generic type parameter (say, T) and then rephrase the narrowing in terms of T. For example:
function expectInstanceOf<T, E, A extends unknown[]>(
obj: T, type: new (...args: A) => E): asserts obj is T & E { }
function expectNotInstanceOf<T, E, A extends unknown[]>(
obj: T, type: new (...args: A) => E): asserts obj is T & Exclude<T, E> { }
Now the example code works as desired:
function foo(obj: Foo | Bar) {
expectInstanceOf(obj, Foo);
obj.foo(); // okay
}
function notFoo(obj: Foo | Bar) {
expectNotInstanceOf(obj, Foo);
obj.bar(); // okay
}
