Overview
I am seeing odd behavior in that the properties that are added to an object through a destructuring assignment (or Object.assign
) are present when passed to forRoot
but are not present when injected into a service. Additionally, updates made after initialization are present when passed to forRoot
but are not present when injected into a service. This only occurs when building with AOT.
I created a minimal project that reproduces the issue: https://github.com/bygrace1986/wat
Package Versions
- angular: 5.2.0
- angular-cli: 1.6.4 (my app), 1.7.4 (my test)
- typescript: 2.4.2
Setup
Create a module that has a static forRoot
method that will accept an object that it will then provide via an InjectionToken
and injected into a service.
TestModule
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TestDependency, TEST_DEPENDENCY, TestService } from './test.service';
@NgModule({
imports: [
CommonModule
],
declarations: []
})
export class TestModule {
static forRoot(dependency?: TestDependency): ModuleWithProviders {
return {
ngModule: TestModule,
providers: [
TestService,
{ provide: TEST_DEPENDENCY, useValue: dependency }
]
}
}
}
TestService
import { Injectable, InjectionToken, Inject } from '@angular/core';
export interface TestDependency {
property: string;
}
export const TEST_DEPENDENCY = new InjectionToken<TestDependency>('TEST_DEPENDENCY');
@Injectable()
export class TestService {
constructor(@Inject(TEST_DEPENDENCY) dependency: TestDependency) {
console.log('[TestService]', dependency);
}
}
Non-mutated Object Scenario
This scenario illustrates that passing an non-mutated object to forRoot
is properly injected into a service that depends on it.
To setup reference the TestService
in the app.component.html
(or somewhere where it will be injected). Pass the dependency to the forRoot
method on the TestModule
.
AppModule
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { TestModule } from './test/test.module';
import { TestDependency } from './test/test.service';
const dependency = {
property: 'value'
} as TestDependency;
console.log('[AppModule]', dependency)
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
TestModule.forRoot(dependency)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Run ng serve --aot
.
Output
[AppModule] {property: "value"}
[TestService] {property: "value"}
Mutated Object Scenario
This scenario illustrates that changes made to an object through object destructuring assignment during initialization or changes made after initialization are ignored when the provided object is injected into a service that depends on it.
To setup create a new object with additional properties and use object destructuring to assign the properties of the old object to the new. Then update a property on dependency
and pass it to the forRoot
method on the TestModule
.
AppModule
const dependency = {
property: 'value'
} as TestDependency;
const dependencyCopy = { id: 1, name: 'first', ...dependency };
dependencyCopy.name = 'last';
console.log('[AppModule]', dependencyCopy);
...
TestModule.forRoot(dependencyCopy)
Run ng serve --aot
.
Output
[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "first"}
Unexpected Result
Any property added by object destructuring assignment was removed and any update made after initialization is reverted between when the object is passed to forRoot
and injected into TestService
. In fact, it isn't event the same object (I debugged and checked with ===
). It is as if the original object that is created before assignment or mutation is being used... somehow.
Mutated Object Provided at AppModule Scenario
This scenario illustrates that mutations to the object are not reverted when provided at the AppModule
level rather than through forRoot
.
To setup don't pass anything to forRoot
. Instead use the injection token to provide the object in the AppModule
providers list.
AppModule
imports: [
BrowserModule,
TestModule.forRoot()
],
providers: [
{ provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],
Run ng serve --aot
.
Output
[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "last", property: "value"}
Question
Why do changes made to an object that is provided via forRoot
get reverted when the object is injected into a dependent class?
Updates
- Realized that updates after initialization are also reverted.
- Realized that this can be reproduced with just the
--aot
flag rather than the broader--prod
flag. - Updated to latest stable cli version (1.7.4) and still have the issue.
- Opened a bug on the
angular-cli
project: https://github.com/angular/angular-cli/issues/10610 - From reading the generated code I think that the issue is in Phase 2 metadata rewriting. When only referencing the variable in
forRoot
I get this:i0.ɵmpd(256, i6.TEST_DEPENDENCY, { id: 1, name: "first" }, [])]);
. When it is referenced in theAppModule
provider list I get this:i0.ɵmpd(256, i6.TEST_DEPENDENCY, i1.ɵ0, [])]);
and then this in the app modulevar ɵ0 = dependencyCopy; exports.ɵ0 = ɵ0;
.