Skip to content

fix(core): infer correct return type when inject is used with generic classes#48408

Closed
markostanimirovic wants to merge 1 commit intoangular:mainfrom
markostanimirovic:fix/core/inject-with-generic-classes
Closed

fix(core): infer correct return type when inject is used with generic classes#48408
markostanimirovic wants to merge 1 commit intoangular:mainfrom
markostanimirovic:fix/core/inject-with-generic-classes

Conversation

@markostanimirovic
Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.io application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #48126

What is the new behavior?

The return type of the inject function is correctly inferred when a class/abstract class with generics is passed as an input argument.

Does this PR introduce a breaking change?

  • Yes
  • No

This commit introduces a breaking change with types when inject is used with generic classes/abstract classes.

BEFORE:

The inferred generic type is always any:

class C<T extends number> {}
abstract class AC<T extends string> {}

const c1 = inject(C); // type: C<any>
const c2 = inject(C<1>); // type: C<any>

const ac1 = inject(AC); // type: AC<any>
const ac2 = inject(AC<'ng'>); // type: AC<any>

AFTER:

The generic type is properly inferred:

class C<T extends number> {}
abstract class AC<T extends string> {}

const c1 = inject(C); // type: C<number>
const c2 = inject(C<1>); // type: C<1>

const ac1 = inject(AC); // type: AC<string>
const ac2 = inject(AC<'ng'>); // type: AC<'ng'>

@alxhub alxhub added the action: global presubmit The PR is in need of a google3 global presubmit label Dec 9, 2022
@markostanimirovic markostanimirovic force-pushed the fix/core/inject-with-generic-classes branch from 4329648 to c94506e Compare December 9, 2022 01:14
@jessicajaniuk jessicajaniuk added the area: core Issues related to the framework runtime label Dec 9, 2022
@ngbot ngbot bot added this to the Backlog milestone Dec 9, 2022
@markostanimirovic markostanimirovic force-pushed the fix/core/inject-with-generic-classes branch from c94506e to efbdaba Compare December 14, 2022 20:52
@pullapprove pullapprove bot requested a review from atscott December 14, 2022 20:52
@markostanimirovic
Copy link
Copy Markdown
Contributor Author

Some of the tests that use injector.get failed because of its last overload signature.

If we pass a class without constructor arguments, the right overload signature will be matched. On the other hand, when we pass a class with constructor arguments, the expected signature will be skipped and the last one (with token: any) will be matched. However, if we comment out the last signature, the right one will be matched. 👀


I reported this behavior here: microsoft/TypeScript#51899
Simplified reproduction:

type Type<T> = new (...args: any[]) => T;

function inject<T>(t: Type<T>): T;
function inject(t: any): any; // comment out this line => the type of `c2` will be `ClassWithCtorArgs`
function inject(t: any): any {
    return t;
}

class Class {}

class ClassWithCtorArgs {
    constructor(x: number) {}
}

// type: Class ✅
const c1 = inject(Class);

// type: any ❌
const c2 = inject(ClassWithCtorArgs);

Before this PR, the right signature was matched because of the { prototype: T } that was part of the AbstractType interface. However { prototype: T } breaks type inference for classes with generics.

@atscott
Copy link
Copy Markdown
Contributor

atscott commented Dec 15, 2022

Given that ClassWithCtorArgs is much more common than classes with generics, I this feels like a regression in the majority of cases. Adding the blocked label until this issue is resolved.

@markostanimirovic
Copy link
Copy Markdown
Contributor Author

Given that ClassWithCtorArgs is much more common than classes with generics, I this feels like a regression in the majority of cases. Adding the blocked label until this issue is resolved.

Yes, for sure. Another solution could be to remove the overload signature with token: any in v16. This signature is currently deprecated btw.

@atscott atscott modified the milestones: Backlog, v16-candidates Dec 15, 2022
@markostanimirovic
Copy link
Copy Markdown
Contributor Author

Hi @pkozlowski-opensource, @atscott 👋

Have you considered removing deprecated signatures with token: any in v16? 👀

@atscott atscott removed this from the v16-candidates milestone Mar 20, 2023
@ngbot ngbot bot added this to the Backlog milestone Mar 20, 2023
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 20, 2023

@markostanimirovic We will not have time to work on a migration for v16.

@UserGalileo
Copy link
Copy Markdown
Contributor

Hi everyone!
With inject becoming more popular, I'm seeing this issue occurring more and more often. Any news about this PR getting merged? 😊 Thanks!

@pavankjadda
Copy link
Copy Markdown
Contributor

I stumbled upon this today. Hopefully this PR gets merged soon!

@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 28, 2024

Per discussions in this PR, the fix here is a regression in the more common cases. The ideal path forward is to remove the overload with token: any

@atscott atscott closed this Mar 28, 2024
@atscott atscott reopened this Mar 29, 2024
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 29, 2024

I think I misunderstood the comment history here. The correct fix requires this change and the removal of the deprecated any overload. Is that correct @markostanimirovic?

@markostanimirovic
Copy link
Copy Markdown
Contributor Author

I think I misunderstood the comment history here. The correct fix requires this change and the removal of the deprecated any overload. Is that correct @markostanimirovic?

Yes 👍

@alxhub alxhub removed this from the v18 feature freeze candidates milestone Apr 11, 2024
@ngbot ngbot bot added this to the Backlog milestone Apr 11, 2024
@LayZeeDK
Copy link
Copy Markdown
Contributor

LayZeeDK commented Aug 15, 2024

@markostanimirovic

Would it be possible to use infer?

#53894 (comment)

@P4
Copy link
Copy Markdown
Contributor

P4 commented Aug 22, 2024

Another possible option is to add the current definition of AbstractType as an overload before the deprecated any:

type Type<T> = new (...args: any[]) => T;

function inject<T>(t: Type<T>): T;
function inject<T>(t: Function & {prototype: T}): T // <- new
function inject(t: any): any;
function inject(t: any): any {
    return t;
}

class ClassWithCtorArgs {
    constructor(x: number) {}
}

// infers ClassWithCtorArgs ✅
const c2 = inject(ClassWithCtorArgs);

The obvious downside is that it adds yet another overload that needs to be removed eventually, and unlike any it can't be marked as deprecated, since that will incorrectly flag usages like ClassWithCtorArgs which will continue working after its removal.

@Harpush
Copy link
Copy Markdown

Harpush commented Mar 3, 2025

Any news concerning this? It makes some signatures really long

atscott added a commit to atscott/angular that referenced this pull request Mar 5, 2025
This commit further restricts the deprecated type on injector.get to exclude
all but `string`. Progresses towards angular#48408

BREAKING CHANGE: The `any` overload has been removed from
`injector.get`. It now only supports `ProviderToken<T>` and (deprecated
since v4) `string`.
AndrewKushnir pushed a commit that referenced this pull request Mar 10, 2025
This commit further restricts the deprecated type on injector.get to exclude
all but `string`. Progresses towards #48408

BREAKING CHANGE: The `any` overload has been removed from
`injector.get`. It now only supports `ProviderToken<T>` and (deprecated
since v4) `string`.

PR Close #60202
@atscott atscott force-pushed the fix/core/inject-with-generic-classes branch from efbdaba to 66940df Compare March 12, 2025 18:11
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 12, 2025

#60202 was submitted to remove the any overload so presumably this should address the regression mentioned in #48408 (comment). Will run global internal tests to see how breaking this change is

@thePunderWoman thePunderWoman marked this pull request as draft May 21, 2025 15:23
…ic classes

This commit sets the correct type for `AbstractType`. With this change, the return type of the `inject` function will be correctly inferred when a class/abstract class with generics is passed as an input argument.

Closes angular#48126
@JeanMeche JeanMeche force-pushed the fix/core/inject-with-generic-classes branch from 66940df to 2400b84 Compare May 21, 2025 15:25
@JeanMeche
Copy link
Copy Markdown
Member

Unfortunately this is way too breaking, components with elementRef = inject(ElementRef); now return unknown and this breaks accesses like component.instance.elementRef.nativeElement.

@JeanMeche JeanMeche closed this May 21, 2025
@angular-automatic-lock-bot
Copy link
Copy Markdown

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.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jun 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: global presubmit The PR is in need of a google3 global presubmit area: core Issues related to the framework runtime state: blocked

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants