question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItΒ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

forwardRef breaks at runtime with ES2015

See original GitHub issue

🐞 bug report

Affected Package

The issue is caused by package @angular/core

Is this a regression?

No.

Description

Using forwardRef as described in https://angular.io/api/core/forwardRef while targeting ES2015 results on a Uncaught ReferenceError: Cannot access 'Lock' before initialization runtime error.

πŸ”¬ Minimal Reproduction

  • make a new project ng new forward-ref-project && cd forward-ref-project
  • ensure tsconfig.json contains "target": "es2015",
  • replace the contents of src/main.ts with:
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
class Door {
  lock: Lock;

  // Door attempts to inject Lock, despite it not being defined yet.
  // forwardRef makes this possible.
  constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}

// Only at this point Lock is defined.
class Lock { }

const injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
const door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);
  • ng serve -o

πŸ”₯ Exception or Error

Uncaught ReferenceError: Cannot access 'Lock' before initialization
    at Module../src/main.ts (main.ts:21)
    at __webpack_require__ (bootstrap:78)
    at Object.2 (main.ts:30)
    at __webpack_require__ (bootstrap:78)
    at checkDeferredModules (bootstrap:45)
    at Array.webpackJsonpCallback [as push] (bootstrap:32)
    at main.js:1

🌍 Your Environment

Angular Version:


Angular CLI: 8.0.0-beta.18
Node: 10.10.0
OS: win32 x64
Angular: 8.0.0-beta.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.800.0-beta.18
@angular-devkit/build-angular     0.800.0-beta.18
@angular-devkit/build-optimizer   0.800.0-beta.18
@angular-devkit/build-webpack     0.800.0-beta.18
@angular-devkit/core              8.0.0-beta.18
@angular-devkit/schematics        8.0.0-beta.18
@angular/cli                      8.0.0-beta.18
@ngtools/webpack                  8.0.0-beta.18
@schematics/angular               8.0.0-beta.18
@schematics/update                0.800.0-beta.18
rxjs                              6.4.0
typescript                        3.4.5
webpack                           4.30.0

Anything else relevant?

forwardRef is provided specifically for the purpose of referencing something that isn’t defined. This is useful in breaking circular dependencies, and when declaring both services and components in the same file.

forwardRef works because it delays the resolution of the reference to a time at which it is already declared through the callback indirection. In the API example, the symbol we want to delay resolution is Lock:

class Door {
  lock: Lock;

  // Door attempts to inject Lock, despite it not being defined yet.
  // forwardRef makes this possible.
  constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}

// Only at this point Lock is defined.
class Lock { }

But Lock is actually being referenced in more places than just inside forwardRef. It is also being used as a TS type in the class property, and in the constructor parameter.

Types don’t usually have a runtime representation so that shouldn’t be a problem. But constructor types are an exception and actually do have a runtime representation. We can see this by looking at the transpiled code:

import * as tslib_1 from "tslib";
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
let Door = class Door {
    // Door attempts to inject Lock, despite it not being defined yet.
    // forwardRef makes this possible.
    constructor(lock) { this.lock = lock; }
};
Door = tslib_1.__decorate([
    tslib_1.__param(0, Inject(forwardRef(() => Lock))),
    tslib_1.__metadata("design:paramtypes", [Lock])
], Door);
// Only at this point Lock is defined.
class Lock {
}
const injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
const door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);

The Lock type in the for the constructor parameter was transpiled into tslib_1.__metadata("design:paramtypes", [Lock]). This reference does not have a delayed resolution like the injected forwardRef and is instead immediately resolved, resulting in Uncaught ReferenceError: Cannot access 'Lock' before initialization.

This error isn’t observed when targetting ES5 however. We can understand why by looking at the code when transpiled to ES5 :

import * as tslib_1 from "tslib";
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
var Door = /** @class */ (function () {
    // Door attempts to inject Lock, despite it not being defined yet.
    // forwardRef makes this possible.
    function Door(lock) {
        this.lock = lock;
    }
    Door = tslib_1.__decorate([
        tslib_1.__param(0, Inject(forwardRef(function () { return Lock; }))),
        tslib_1.__metadata("design:paramtypes", [Lock])
    ], Door);
    return Door;
}());
// Only at this point Lock is defined.
var Lock = /** @class */ (function () {
    function Lock() {
    }
    return Lock;
}());
var injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
var door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);

In ES5 there are no class declarations, so TS instead uses a var. One important different between var and class/let/const is that the latter are all subject to the Temporal Dead Zone.

In practical terms the TDZ means that using a var before it is declared resolves to undefined, but using a class/let/const instead throws a ReferenceError. This is the error we are seeing here.

A possible workaround is to not declare the type in the constructor:

// Instead of adding the type in the parameter
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { 
  this.lock = lock;
}

// Add it as a cast in the constructor body
constructor(@Inject(forwardRef(() => Lock)) lock) {
  this.lock = lock as Lock;
}

This will change the transpiled code and remove the reference, avoiding the ReferenceError:

Door = tslib_1.__decorate([
    tslib_1.__param(0, Inject(forwardRef(() => Lock))),
    tslib_1.__metadata("design:paramtypes", [Object])
                                             ^^^^^^ was Lock before
], Door);

One important note is that the ReferenceError does not come up on Angular CLI projects compiled with AOT. This is because there we actually transform transpiled TS code and remove Angular decorators, so the metadata reference (tslib_1.__metadata("design:paramtypes", [Lock])) never reaches the browser and thus there is no ReferenceError.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:23
  • Comments:19 (7 by maintainers)

github_iconTop GitHub Comments

5reactions
DmitryEfimenkocommented, Mar 5, 2020

+1

5reactions
elvisbegoviccommented, Apr 25, 2019

I think (in v8) at runtime, a lot of people may be suprised targeting es2015 by default due to the fact CLI historically allowed "showCircularDependencies": false and consider circular deps as warning.

This will breaks a lot of existing angular project, for sure

Read more comments on GitHub >

github_iconTop Results From Across the Web

Forward references in Angular | Articles by thoughtram
In this article we like to explore forward references. Why they exist and how we can use them.
Read more >
Why is ForwardRef causing error when using pair tag
The solution, react.forwardRef takes two generic arguments, the first is the type of DOM element your will be forwarding the ref too, and...
Read more >
Invalid Hook Call Warning - React
Breaking the Rules of Hooks. You can only call Hooks while React is rendering a function component: βœ“ Call them at the top...
Read more >
Angular 12 in Depth - SΓ©bastien Dubois
It includes a number of breaking changes, which explains why it took a while for Angular and a gazillion utilities/libraries in the ecosystem...
Read more >
Read more - GitLab
isArray(arr)) return arr; - } - - module.exports = _arrayWithHoles; -},13,[],"node_modules/@babel/runtime/helpers/arrayWithHoles.js"); -__d(function (globalΒ ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found