Home > Software design >  Negative type assertion return predicate
Negative type assertion return predicate

Time:09-23

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
}

Playground link to code

  • Related