8

I'm modeling data in typescript sent from a server to my Angular app. An Opportunity has a forms property containing an array of Form[] objects. A Form has a parent property which may contain an Opportunity. In order to resolve the types, the file which defines Opportunity imports Form and the file which defines Form imports Opportunity. This creates a circular dependency warning.

I've found several prior SO questions dealing with circular dependencies (here, here), but in each case they were dealing with circular dependencies in the javascript code. In this case, the circular dependency is only with the typescript types, and will not exist after compilation. Is there some way to include a type in a file while avoiding this circular dependency issue? So far I haven't found anything.

I can think of two solutions to this problem:

  1. Define both models in the same file
  2. Recreate the Form interface in the Opportunity file / the Opportunity interface in the Form file.

Are there any other / better solutions? Thanks!

Update 2

I appear to have found an answer (it was just really far down in the list of questions for some reason). This answer suggestions two possibilities

  1. Create a seperate definition file (which would seem to involve recreating the Opportunity and Form class interfaces, so would be no better then option #2 above).

  2. Use import, which is what I'm already doing (and which is causing the circular dependency warning).

Is there a way to import just the associated interface of a class?

Update 3

Just to be clear, currently Opportunity and Form look like this:

// opportunity.ts
import { Form } from '....../form'

export class Opportunity {
  public forms: Form[] = [];
}

// form.ts
import { Opportunity } from '....../opportunity'

export class Form {
  public parent: Opportunity;
}
Community
  • 1
  • 1
John
  • 9,249
  • 5
  • 44
  • 76
  • You can declare a class `declare class Opportunity {}` in `form.ts` file, TypeScript will assume that class is an external class and will be available at runtime. And you can skip import in one of the class. – Akash Kava Jan 24 '18 at 09:48
  • 1
    @AkashKava thanks!! I can accept this answer, and maybe this is the best solution. It's not _perfect_ though because declaring the class in this manner removes various autocomplete options from my IDE (VS Code). I assume because the IDE no longer knows anything about the declared class. In order to recapture the autocomplete ability, is the only solution to manually redefine the interface? – John Jan 24 '18 at 09:55

4 Answers4

11

from typescript version 3.8

import type would help you.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html

mozu
  • 729
  • 1
  • 7
  • 10
4

You can declare a class declare class Opportunity {} in form.ts file, TypeScript will assume that class is an external class and will be available at runtime. And you can skip import in one of the class.

The only pain here is, you have to declare methods that you will be using for example,

declare class Opportunity {
     method1(): void;
     method2(): number;
}

This class will serve as simple declaration and will not require method body. And VS intellisense will work correctly.

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
1

You could remove the circular dependency by depending on abstractions, for example your form module could define an interface or abstract class that parent must confirm to.

This would mean your opportunity module would depend on the form module, but not vice versa. You would inject a concrete Opportunity into the Form.parent property and that would be acceptable.

This also allows you to define a FormParent (see below) that is a subset of Opportunity as you probably don't depend on everything in Opportunity.

As TypeScript is structural, it is up to you whether you explicitly implement the interface on the Opportunity class.

// opportunity.ts

import { Form } from '....../form'

export class Opportunity {
  public forms: Form[] = [];
}

// form.ts

interface FormParent {
    forms: Form[];
}

export class Form {
  public parent: FormParent;
}
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • This is _also_ a very good answer! My question was a slight simplification. On `Form` I actually have `public parent: FormParent;` where `type FormParent = Opportunity | ContactList;`. It didn't occur to me that I could make `FormParent` into an abstract class which other classes could extend. I'll definitely keep this idea in mind. However, when pulling a `Form` from the server, the JSON may contain args to construct the parent property. One advantage of the accepted answer is that, in the `Form` constructor, I can instantiate a `new Opportunity()` because opportunity has been declared. – John Jan 24 '18 at 11:28
1

Another approach is to use require instead of import in one class.
Looks something like:

let LoginPage = require("./loginpage").default;
return new LoginPage();

You can find it in the Best Practice #5 from this post.

סטנלי גרונן
  • 2,917
  • 23
  • 46
  • 68