Definitelytyped: [@types/jasmine] spyOn with this results in Argument of type '"..."' is not assignable to parameter ...

Created on 4 Jul 2019  路  5Comments  路  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/xxxx package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [ ] I have a question that is inappropriate for StackOverflow.
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @fdim @kolodny (didn't know who to pick and it seemed a bit excessive to mention all, so I picked two random names out of the list, hope that's appropriate)

I have a freshly updated Angular project with the latest TypeScript 3.5.2 and jasmine 3.3.13. During testing, I got an error with one of my mock objects.

export class AuthServiceMock {
    constructor() {
        spyOn(this, 'logoff');
    }

    logoff() { }
}

This test code used to work, but now using spyOn() gives an error:

Argument of type '"logoff"' is not assignable to parameter of type 'this["logoff"] extends Function ? "logoff" : never'.ts(2345)

It's easily circumventable using as:

spyOn(this as AuthServiceMock, 'logoff');

But this shouldn't be necessary, so I thought I'd share my findings.

Most helpful comment

I believe this is a Typescript error, I was able to get a minimal repro case of what's happening with this example:

export class MyClass {
  constructor() {
    getMethod(this, 'logoff') // Error
    getMethod(this as MyClass, 'logoff') // OK
  }

  logoff() {}
}

function getMethod<T, K extends keyof T = keyof T>(
  object: T,
  method: T[K] extends Function ? K : never,
) {
  return object[method];
}

I've created https://github.com/microsoft/TypeScript/issues/32258 on the main TS repo

All 5 comments

Interesting usage, I am not sure about the issue, but have you tried to use export const AuthServiceMock = jasmine.createSpyObj<Auth service>('AuthService',['logoff'])?

Normally I keep mocks inside the spec.

Anyway, I'll try to reproduce the issue a bit later

I believe this is a Typescript error, I was able to get a minimal repro case of what's happening with this example:

export class MyClass {
  constructor() {
    getMethod(this, 'logoff') // Error
    getMethod(this as MyClass, 'logoff') // OK
  }

  logoff() {}
}

function getMethod<T, K extends keyof T = keyof T>(
  object: T,
  method: T[K] extends Function ? K : never,
) {
  return object[method];
}

I've created https://github.com/microsoft/TypeScript/issues/32258 on the main TS repo

@FDIM:

Interesting usage, I am not sure about the issue, but have you tried to use export const AuthServiceMock = jasmine.createSpyObj<Auth service>('AuthService',['logoff'])?

I'm using this mock with Angular's dependency injection mechanism, which needs types. But there are definitely cases where this is a viable solution.

Normally I keep mocks inside the spec.

I do as well for most mocks. Only with a handful of them I noticed I was duplicating mock behavior, so I created separate mock classes.

@kolodny:

I believe this is a Typescript error, I was able to get a minimal repro case of what's happening with this example:
...
I've created microsoft/TypeScript#32258 on the main TS repo

Ah, awesome. Thanks!

Do you have any updates? I can see the TS bug has been closed down with no resolution.
@FDIM @kolodny

This is still blocked by https://github.com/microsoft/TypeScript/issues/33179

I took a stab at it but can't seem to get it to work at this time, feel free to take over on this effort:

diff --git a/types/jasmine/index.d.ts b/types/jasmine/index.d.ts
index 40706dec76..398f4accbf 100644
--- a/types/jasmine/index.d.ts
+++ b/types/jasmine/index.d.ts
@@ -160,7 +160,10 @@ interface DoneFn extends Function {
  * @param object The object upon which to install the `Spy`.
  * @param method The name of the method to replace with a `Spy`.
  */
-declare function spyOn<T>(object: T, method: keyof T): jasmine.Spy;
+declare function spyOn<T, K extends keyof T = keyof T>(
+    object: Record<K, Function>,
+    method: keyof T extends never ? string : K,
+): jasmine.Spy;

 /**
  * Install a spy on a property installed with `Object.defineProperty` onto an existing object.
diff --git a/types/jasmine/jasmine-tests.ts b/types/jasmine/jasmine-tests.ts
index c6220c548f..9e6beb3654 100644
--- a/types/jasmine/jasmine-tests.ts
+++ b/types/jasmine/jasmine-tests.ts
@@ -339,6 +339,15 @@ describe("A spy", () => {
         foo.setBaz(789);
     });

+    it('works with this in the constructor', () => {
+        class MyClass {
+            constructor() {
+                spyOn(this, 'logoff');
+            }
+            logoff() {}
+        }
+    });
+
     it("tracks that the spy was called", () => {
         expect(foo.setBar).toHaveBeenCalled();
     });
diff --git a/types/jasmine/ts3.1/index.d.ts b/types/jasmine/ts3.1/index.d.ts
index e5f6799418..3f9df3a1e8 100644
--- a/types/jasmine/ts3.1/index.d.ts
+++ b/types/jasmine/ts3.1/index.d.ts
@@ -157,9 +157,11 @@ interface DoneFn extends Function {
  * @param object The object upon which to install the `Spy`.
  * @param method The name of the method to replace with a `Spy`.
  */
-declare function spyOn<T, K extends keyof T = keyof T>(
-    object: T, method: T[K] extends Function ? K : never,
+declare function spyOn<T, K extends keyof T>(
+    object: Record<K, Function>,
+    method: keyof T extends never ? string : K,
 ): jasmine.Spy<
+    keyof T extends never ? any :
     T[K] extends InferableFunction ? T[K] :
     T[K] extends {new (...args: infer A): infer V} ? (...args: A) => V :
     T[K] extends Function ? InferableFunction : never

Was this page helpful?
0 / 5 - 0 ratings