-
Notifications
You must be signed in to change notification settings - Fork 27.2k
Description
Which @angular/* package(s) are the source of the bug?
compiler
Is this a regression?
No
Description
This reproduction demonstrates an Angular compiler issue where a component imported from another library fails validation if its imports array contains another array built with the spread operator.
- Create workspace
ng new app --directory=./ --minimal --style=css --ssr=false --zoneless=true --ai-config=noneng generate library lib-ang generate library lib-bng generate library lib-c- Fill
projects/lib-a/src/lib/lib-a.ts:
import { Directive } from '@angular/core';
@Directive({ selector: '[baseEntity1]' })
export class BaseEntity1 {}
@Directive({ selector: '[baseEntity2]' })
export class BaseEntity2 {}
export const BaseEntities = [
BaseEntity1,
BaseEntity2,
] as const;- Fill
projects/lib-b/src/lib/lib-b.ts:
import { Directive } from '@angular/core';
import { BaseEntities } from 'lib-a';
@Directive({selector: '[moreComplexEntity]'})
export class MoreComplexEntity {}
export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;- Fill
projects/lib-с/src/lib/lib-c.ts:
import {Component} from '@angular/core';
import {UserFriendlyImport} from 'lib-b';
@Component({
selector: 'comp-from-library',
imports: [UserFriendlyImport],
template: `
<div baseEntity2>123</div>
<div moreComplexEntity>123</div>
`,
})
export class LibraryComponent {}- Replace all
src/app/app.tswith the following content:
import {Component} from '@angular/core';
import {LibraryComponent} from 'lib-c';
@Component({
selector: 'app-root',
imports: [LibraryComponent],
template: `<comp-from-library />`,
})
export class App {}- Run
ng build lib-a && ng build lib-b && ng build lib-c && ng build appThe application build fails with:
✘ [ERROR] NG2012: Component imports must be standalone components, directives, pipes, or must be NgModules. [plugin angular-compiler]
src/app/app.ts:6:12:
6 │ imports: [LibraryComponent],
╵ ~~~~~~~~~~~~~~~~
- Make these changes inside
projects/lib-b/src/lib/lib-b.ts
- export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;
+ export const UserFriendlyImport = [BaseEntity1, BaseEntity2, MoreComplexEntity] as const;No error!
This confirms that the issue is related specifically to using the spread operator in a shared constant that is later consumed via imports.
Please provide a link to a minimal reproduction of the bug
Please provide the exception or error you saw
✘ [ERROR] NG2012: Component imports must be standalone components, directives, pipes, or must be NgModules. [plugin angular-compiler]
src/app/app.ts:6:12:
6 │ imports: [LibraryComponent],
╵ ~~~~~~~~~~~~~~~~
Please provide the environment you discovered this bug in (run ng version)
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI : 21.0.0
Angular : 21.0.1
Node.js : 24.10.0
Package Manager : npm 11.6.1
Operating System : darwin arm64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.0.0 │ ^21.0.0 │
│ @angular/cli │ 21.0.0 │ ^21.0.0 │
│ @angular/common │ 21.0.1 │ ^21.0.0 │
│ @angular/compiler │ 21.0.1 │ ^21.0.0 │
│ @angular/compiler-cli │ 21.0.1 │ ^21.0.0 │
│ @angular/core │ 21.0.1 │ ^21.0.0 │
│ @angular/forms │ 21.0.1 │ ^21.0.0 │
│ @angular/platform-browser │ 21.0.1 │ ^21.0.0 │
│ @angular/router │ 21.0.1 │ ^21.0.0 │
│ ng-packagr │ 21.0.0 │ ^21.0.0 │
│ rxjs │ 7.8.2 │ ~7.8.0 │
│ typescript │ 5.9.3 │ ~5.9.2 │
│ vitest │ 4.0.14 │ ^4.0.8 │
└───────────────────────────┴───────────────────┴───────────────────┘
Anything else?
Why we use [...] as const approach ?
In Taiga UI (Angular UI library), many apparent "single" components are actually composed from multiple components and directives. With NgModules this complexity was encapsulated: consumers imported one module and were done.
But with standalone components, this changed. Now, library consumers needed to import every individual piece manually — which wasn’t great for DX. IDEs like WebStorm started suggesting dozens of imports, and that quickly got annoying.
To restore a single, ergonomic entry point, we group related declarations into as const arrays.
For example:
export const TuiTextfield = [
TuiLabel,
TuiTextfieldComponent,
TuiTextfieldDirective,
TuiTextfieldOptionsDirective,
] as const;This keeps the public API compact: the IDE suggests one symbol, while the array still contains all necessary pieces.
Why spread support is important?
The non-spread workaround:
- export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;
+ export const UserFriendlyImport = [BaseEntity1, BaseEntity2, MoreComplexEntity] as const;But in real-world projects with dozens of components and directives, manually listing each one becomes cumbersome and error-prone. The spread operator allows us to maintain modularity and reusability without sacrificing developer experience.
Just explore how we have to violate DRY in Taiga UI now only for textfield components:
That PR shows extra duplication just for one type of components. With dozens of similar components, the maintenance overhead becomes significant. Supporting spread-merged arrays in imports would allow Angular libraries to stay modular and maintainable without sacrificing DX.