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 to
unknown`, 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
}