Files
Andrew Scott caeab598c9 refactor(compiler): emit any as type argument for ɵɵInjectableDeclaration
The static `ɵprov` field emitted on `@Injectable()` classes uses `ɵɵInjectableDeclaration<T>`.
When a subclass extends a generic `@Injectable()` base class with contravariant parameters
(such as callback/transformer methods depending on generic type parameters), TypeScript's
static side inheritance check (`typeof Sub extends typeof Super`) fails with `TS2417` because
`ɵɵInjectableDeclaration<Sub>` is not assignable to `ɵɵInjectableDeclaration<Super<any>>`.

Using `any` (`o.DYNAMIC_TYPE`) in `createInjectableType` avoids strict variance checks on
static inheritance for internal Ivy definitions and aligns with other Ivy declaration types.
2026-09-04 07:05:23 -07:00

128 lines
3.7 KiB
TypeScript

/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('@Service decorator', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});
// More thorough compilation tests are in `packages/compiler-cli/test/compliance/test_cases/service_decorator`.
it('should compile an @Service class', () => {
env.write(
'test.ts',
`
import {Service} from '@angular/core';
@Service()
export class TestService {}
`,
);
env.driveMain();
const jsContents = env.getContents('test.js');
const dtsContents = env.getContents('test.d.ts');
expect(jsContents).not.toContain('__decorate');
expect(jsContents).toContain('TestService.ɵfac =');
expect(jsContents).toContain('TestService.ɵprov =');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<TestService, never>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration<any>;');
});
it('should report if an @Service class has another Angular decorator', () => {
env.write(
'test.ts',
`
import {Service, Pipe} from '@angular/core';
@Service()
@Pipe({name: 'foo'})
export class TestService {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'Cannot apply more than one Angular decorator on an @Service class.',
);
});
it('should report if an @Service class uses constructor-based DI from its own constructor', () => {
env.write(
'test.ts',
`
import {Service, ApplicationRef} from '@angular/core';
@Service()
export class TestService {
constructor(appRef: ApplicationRef) {}
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'@Service class cannot use constructor dependency injection. Use the `inject` function instead.',
);
});
it('should report if an @Service class uses constructor-based DI from an inherited constructor', () => {
env.write(
'grandparent.ts',
`
import {Injectable, ApplicationRef} from '@angular/core';
@Injectable()
export class GrandparentService {
constructor(appRef: ApplicationRef) {}
}
`,
);
env.write(
'parent.ts',
`
import {Injectable} from '@angular/core';
import {GrandparentService} from './grandparent';
@Injectable()
export class ParentService extends GrandparentService {}
`,
);
env.write(
'test.ts',
`
import {Service} from '@angular/core';
import {ParentService} from './parent';
@Service()
export class TestService extends ParentService {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'@Service class cannot use constructor dependency injection. Use the `inject` function instead.',
);
});
});
});