Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error when using TypeScript classes that depend on each other #10712

Closed
KoenT-X opened this issue Sep 5, 2016 · 4 comments
Closed

Error when using TypeScript classes that depend on each other #10712

KoenT-X opened this issue Sep 5, 2016 · 4 comments

Comments

@KoenT-X
Copy link

KoenT-X commented Sep 5, 2016

In a medium-sized project (single page web app client), we are running into a blocking issue with TypeScript when two classes depend on each other.

I have created a very simple minimal example to demonstrate the problem: see the code below, or download the fully self-contained code (with VS2015 project) here:
https://bitbucket.org/KoenT_IM/typescripttests/src/93e810457a5d1cf9286ea17cfe2a0ef6595070ec/Applications/MutualDependenciesTest/?at=master

TypeScript Version: 1.8.36.0

Code

ClassA.ts

import ClassB = require("ClassB");

class ClassA
{
    doAction()
    {
        console.log("Logs from ClassA.doAction:");
        console.log("ClassA parameter name  = " + ClassA.ParameterNames.SomeName);
        console.log("ClassB parameter name  = " + ClassB.ParameterNames.SomeName);
    }

    static ParameterNames =
    {
        SomeName: "ClassA string"
    }
}

export = ClassA;

ClassB.ts

import ClassA = require("ClassA");

class ClassB
{
    doAction()
    {
        console.log("Logs from ClassB.doAction:");
        console.log("ClassA parameter name  = " + ClassA.ParameterNames.SomeName); // exception here: JavaScript runtime error: "Unable to get property 'SomeName' of undefined or null reference" (ClassA.ParameterNames is undefined)
        console.log("ClassB parameter name  = " + ClassB.ParameterNames.SomeName);
    }

    static ParameterNames =
    {
        SomeName: "ClassB string"
    }
}

export = ClassB;

MainClass.ts

import ClassA = require("ClassA");
import ClassB = require("ClassB");

class MainClass
{
    constructor()
    {
        console.log("Logs from MainClass:");
        console.log("ClassA parameter name  = " + ClassA.ParameterNames.SomeName);
        console.log("ClassB parameter name  = " + ClassB.ParameterNames.SomeName);

        let objectA = new ClassA();
        let objectB = new ClassB();
        objectA.doAction();
        objectB.doAction(); // exception in here
    }
}

export = MainClass;

app.ts

import MainClass = require("MainClass");

let main = new MainClass();

index.html

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Mutual dependencies test</title>
</head>
<body>
    <h1>Mutual dependencies test</h1>
    <script src="scripts/require.js" data-main="app"></script>
</body>
</html>

Expected behavior:

Logs from MainClass:
ClassA parameter name  = ClassA string
ClassB parameter name  = ClassB string
Logs from ClassA.doAction:
ClassA parameter name  = ClassA string
ClassB parameter name  = ClassB string
Logs from ClassB.doAction:
ClassA parameter name  = ClassA string
ClassB parameter name  = ClassB string

Actual behavior:

Logs from MainClass:
ClassA parameter name  = ClassA string
ClassB parameter name  = ClassB string
Logs from ClassA.doAction:
ClassA parameter name  = ClassA string
ClassB parameter name  = ClassB string
Logs from ClassB.doAction:
SCRIPT5007: Unhandled exception at line 8, column 13 in http://localhost:59304/ClassB.js
0x800a138f - JavaScript runtime error: Unable to get property 'SomeName' of undefined or null reference
ClassB.ts (8,9)

The project/code builds without any errors/warnings, but at runtime, execution fails.

What happens is that in the ClassB.doAction() call, ClassA is an "empty object", and ClassA.ParameterNames is undefined.

So, from a developer point of view: doing import ClassA = require("ClassA"); in MainClass.ts lets you use ClassA.ParameterNames.SomeName as expected, whereas doing that exact same import in another TypeScript file (ClassB.ts) and using the exact same code ClassA.ParameterNames.SomeName compiles but fails at runtime.

@normalser
Copy link

That's not an issue with TypeScript. Seems like you are using requirejs so read this: http://requirejs.org/docs/api.html#circular
Rethink your design or dont use export = - use like export class ClassA

@KoenT-X
Copy link
Author

KoenT-X commented Sep 5, 2016

OK, thanks for the response!
Shouldn't there be some kind of warning/error then, when the JS code is generated? This behavior seems very tricky...

The design is fine: while in a few simple cases where this happens it might be possible to isolate some parts into a separate common file, in other cases, the 2 (or more) classes really need to work together and be able to access functionality/properties of each other.

I tried removing the export = ...; statements, and changing the class ... statements into export class ... statements. While this seems to work now, I get code looking like this:

import MainClass = require("MainClass");

let main = new MainClass.MainClass();

or

import ClassA = require("ClassA");
import ClassB = require("ClassB");

export class MainClass
{
    constructor()
    {
        console.log("Logs from MainClass:");
        console.log("ClassA parameter name  = " + ClassA.ClassA.ParameterNames.SomeName);
        console.log("ClassB parameter name  = " + ClassB.ClassB.ParameterNames.SomeName);

        let objectA = new ClassA.ClassA();
        let objectB = new ClassB.ClassB();
        objectA.doAction();
        objectB.doAction(); // exception in here
    }
}

which is terrible (especially if you have to do that throughout your whole codebase).
Am I missing something?

@kitsonk
Copy link
Contributor

kitsonk commented Sep 5, 2016

Shouldn't there be some kind of warning/error then, when the JS code is generated?

That would make it so TypeScript has to predict how the modules will be loaded and the capabilities of the module loader. Cross module circular references are really complex (and are largely considered bad design practice) and each module loader has different levels of support and resolution logic for such things.

You are also conflating circular references with "old" TypeScript module import syntax and the better supported ES2015/ES6 syntax. For example, you should use:

import { MainClass } from "MainClass";

let main = new MainClass();

If the main thing exported from "MainClass" is MainClass then in MainClass.ts:

export default class MainClass {
  /* class here */
}

and then when you want to import it:

import MainClass from "MainClass";

let main = new MainClass();

And then when TypeScript emits to AMD, it will resolve all those names for you, and makes transition to ES2015 modules easier in the future. Nothing "ugly" about that.

@KoenT-X
Copy link
Author

KoenT-X commented Sep 5, 2016

Thanks @kitsonk ! I had tried export default class ... but the import ... from "..." was what was needed to make it work. This solved the problem in this example, and most probably will in our more complex real-life project. I'm closing the issue.
All these different types of code combining / module loading make things so finicky, but that's probably just me (coming from a C++/C# background)...

@KoenT-X KoenT-X closed this as completed Sep 5, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants