Skip to content

feat: typed calls to inject() testing helper #18695

Closed

Description

@dherges

I'm submitting a Feature Request


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Type informations for the arrow-function need to be added explicitly.

inject([A, B, C], (a: A, b: B, c: C) => {
  /* (parameter) a: A */
  /* .. */
});

If ommited, params a, b, and c are of type any:

inject([A, B, C], (a, b, c) => {
  /* (parameter) a: any */
  /* .. */
});

See https://github.com/angular/angular/blob/master/packages/core/testing/src/test_bed.ts#L472-L491

Expected behavior

As an improvement (new feature), it would be cool if calls to inject() are typed.

My first idea is to do it similar to the rxjs operators like .of():

static of<T>(item1: T, scheduler?: IScheduler): Observable<T>;
static of<T>(item1: T, item2: T, scheduler?: IScheduler): Observable<T>;
static of<T>(item1: T, item2: T, item3: T, scheduler?: IScheduler): Observable<T>;
static of<T>(...array: Array<T | IScheduler>): Observable<T>;

For inject() then it looks w/ help of the Type<T> from angular core:

static inject<T1>(item1: Type<T1>, (t: T1) => void): () => any;
static inject<T1, T2>(item1: Type<T1>, item2: Type<T2>, (t1: T1, t2: T2) => any): () => any;
static inject<T1, T2, T3>(item1: Type<T1>, item2: Type<T2>, item3: Type<T3>, (t1: T1, t2: T2, t3: T3) => any): () => any;

Here is a proof-of-concept on the TypeScript playground

The inject() helper should be added for up to 7 (???) params. A fallback with the array notation should remain. Concrete threshold can be discussed! RxJS .of() does it for up to 6 params and the array notation is the seventh version as a fallback.

What do you think?

I'd like to get some feedback before opening a PR! Thanks! 🧀

Minimal reproduction of the problem with instructions

See above.

What is the motivation / use case for changing the behavior?

See above. Better dev experience. 🌵 Improve type support in unit testing. 🌴

Environment


Angular version: 4.3.3

Activity

  1. dherges commented on Aug 15, 2017

    @dherges
    ContributorAuthor

    Little bit more draft code:

    function inject<T1>(item1: Type<T1>, fn: (t1: T1) => any): any;
    function inject<T1, T2>(item1: Type<T1>, item2: Type<T2>, fn: (t1: T1, t2: T2) => any): any;
    
    function inject(...params: any[]) {
    
      let types: any[];
      if (params[0] instanceof Array) {
        types = [ params[0] ];
      } else {
        types = params.slice(0, params.length - 1);
      }
    
      const fn = params[params.length - 1];
      if (!(fn instanceof Function)) {
          throw new TypeError("Expected last argument to be a function");
      }
    
    
      /* ... current impl ... */
      return function () {
        /* ... current implementation ... */
      }
    }
  2. added
    area: coreIssues related to the framework runtime
    on Aug 16, 2017
  3. added
    area: testingIssues related to Angular testing features, such as TestBed
    and removed
    area: coreIssues related to the framework runtime
    on Aug 17, 2017
  4. dherges commented on Aug 21, 2017

    @dherges
    ContributorAuthor

    Ping. Please give some feedback.

  5. dherges commented on Aug 23, 2017

    @dherges
    ContributorAuthor

    Can you please give some feedback?

  6. dherges commented on Aug 28, 2017

    @dherges
    ContributorAuthor

    Hello Angular team,

    I know you have been on a sailing tour over the weekend and hope you enjoyed it!

    Now with the fresh week starting, getting a bit feedback here would be super cool!

  7. dherges commented on Sep 1, 2017

    @dherges
    ContributorAuthor

    Dear Angular team,

    today is Friday 1st September 2017. Unfortunately, there was no feedback until now.

    I hope for better luck in the next week!

    Cheers!

  8. dherges commented on Sep 12, 2017

    @dherges
    ContributorAuthor

    I'd really appreciate any feedback! It could be a "good to have this" or "no, we don't want this" or a "someone else is already working on another rewrite" ...

  9. dherges commented on Sep 21, 2017

    @dherges
    ContributorAuthor

    🕣 👩‍🍳 🍪

  10. dherges commented on Sep 26, 2017

    @dherges
    ContributorAuthor

    Here is a wrapper implementation around the current inject:

    import { Type } from '@angular/core';
    import { inject as ngInject } from '@angular/core/testing';
    
    function inject<T1>(item1: Type<T1>, fn: (t1: T1) => any): any;
    function inject<T1, T2>(item1: Type<T1>, item2: Type<T2>, fn: (t1: T1, t2: T2) => any): any;
    /* ... add remaining declarations here... */
    
    export function inject(...params: any[]) {
    
      let types: any[];
      if (params[0] instanceof Array) {
        types = [ params[0] ];
      } else {
        types = params.slice(0, params.length - 1);
      }
    
      const fn = params[params.length - 1];
      if (!(fn instanceof Function)) {
          throw new TypeError("Expected last argument to be a function");
      }
    
      return ngInject(types, fn);
    }

    Usage:

    inject(MyService, (m) => {
      /** m is inferred as type "MyService" */
    });

    2017-09-26 07_15_08-greenshot

    Was before:

    inject([MyService], (m: MyService) => {
      /* .. */
    });
  11. dherges commented on Sep 28, 2017

    @dherges
    ContributorAuthor

    Here is an sissue with the above:

    inject(HttpTestingController, (http) => { .. });
    Argument of type 'typeof HttpTestingController' is not assignable to parameter of type 'Type<HttpTestingController>'.
      Cannot assign an abstract constructor type to a non-abstract constructor type.
    
  12. GregOnNet commented on Oct 23, 2017

    @GregOnNet

    @dherges I experienced the same issue writing a strictly typed wrapper for inject.

    My understanding is that we cannot tell Typescript that an instance of a certain type is going to be created by another method since TypeScript is based on a static type system.

    I think the origin inject method has to be typed.

    The benefit of typing inject is like you said:

    • reduce boilerplate
    • tests are better to read
    • it becomes easier to write factory methods for tests based on inject

    I would love to see this in Angular, too. ❤️

    Kind regards
    Greg

  13. dherges commented on Oct 25, 2017

    @dherges
    ContributorAuthor

    @GregOnNet Yes. The issue is that Type<T> has a call signature for new T() (creating a new class instance). But an abstract class cannot be created, so it's not a Type<?>.

    Maybe it would work with Type<T> | InjectionToken<T>.

    But for exmaple, the HttpTestingController is provided here: https://github.com/angular/angular/blob/master/packages/common/http/testing/src/module.ts#L29-L30

    {provide: HttpBackend, useExisting: HttpClientTestingBackend},
    {provide: HttpTestingController, useExisting: HttpClientTestingBackend},

    and ExistingProvider is declared as https://angular.io/api/core/ExistingProvider

    So maybe it would need to be Type<T> | InjectionToken<T> | any to cover all cases?

  14. GregOnNet commented on Oct 25, 2017

    @GregOnNet

    Hey,

    I thank you so much for your input.
    I think I found a solution.

    A prototype getting everything to work can be found here: https://gist.github.com/GregOnNet/220ae5e53843313e541a4f6ae0b35183

    Later this week I will prepare a general solution.

    Kinds
    Greg

  15. devoto13 commented on Nov 17, 2017

    @devoto13
    Contributor

    @dherges You can workaround abstract classes using approach described in microsoft/TypeScript#5843 (comment). Note | Function added to the Type signature.

    Code example you posted here worked correctly with HttpTestingController, when I replaced Type imported from Angular with type Type<T> = { new (...args: any[]): T } | Function;. Everything is getting typed correctly as well.

  16. added
    featureLabel used to distinguish feature request from other issues
    on Jan 2, 2018
  17. jasonaden commented on Jan 2, 2018

    @jasonaden
    Contributor

    @vikerman Any opinion on this one?

  18. added this to the Backlog milestone on Jan 23, 2018
  19. devoto13 commented on Oct 16, 2018

    @devoto13
    Contributor

    With TypeScript 3.1 this can be expressed even easier using mapped tuple types. Below is the typed wrapper of the inject() function, which I use in my projects in the meantime:

    import { inject as ngInject } from '@angular/core/testing';
    
    type Type<T> = { new(...args: any[]): T } | Function;
    
    type MapToType<T> = { [K in keyof T]: Type<T[K]>};
    
    export function inject<T extends any[]>(types: MapToType<T>, fn: (...params: T) => any): () => any {
      return ngInject(types, fn);
    }

    It can be used exactly the same as the original function:

    class MyClass {
      myMethod() { return 42; }
    }
    
    it('works', inject([MyClass], (myClass) => myClass.myMethod()));
  20. angular-robot commented on Jun 4, 2021

    @angular-robot
    Contributor

    Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

    Find more details about Angular's feature request process in our documentation.

  21. angular-robot commented on Jun 24, 2021

    @angular-robot
    Contributor

    Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage.

    We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package.

    You can find more details about the feature request process in our documentation.

  22. angular-automatic-lock-bot commented on Apr 28, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  23. locked and limited conversation to collaborators on Apr 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: testingIssues related to Angular testing features, such as TestBedfeatureLabel used to distinguish feature request from other issuesfreq3: high

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions