Circular Type References in TypeScript
Asked Answered
L

3

71

I am new to typescript and am trying to understand how I can setup a circular reference between two types. The reference need not be a full code reference, simply the interfaces, but with interfaces defined in separate files. For example, let's say I have two interfaces: Parent and Child. They are doubly-linked such that the parent has a collection of children and each child has a reference to the parent (as seen below). How do I setup the imports or dependencies so that these can be defined in separate files?

interface Parent {
  children: Child[]
}

interface Child {
  parent: Parent
}
Leptophyllous answered 27/6, 2014 at 5:17 Comment(2)
I would personally put them in the same file because they are inextricably linked and are both erased during compilation.Stephanus
For ways to solve circular dependencies refer to this question: https://mcmap.net/q/118193/-typeerror-class-extends-value-undefined-is-not-a-function-or-nullTempo
C
22

Two solutions below. I prefer the latter since it offers clean interfacing with Node JS modules, but unfortunately my IDE doesn't (yet) like it as much as I do...

Use references

Create a definitions.d.ts file that will only contain the references to your classes/interfaces

/// <reference path="Parent.ts" />
/// <reference path="Child.ts" />

In Parent.ts and Child.ts, point to a single reference, the definitions.d.ts file

/// <reference path="definitions.d.ts" />

Use import...require

pass the --module commonjs flag to tsc then import what you require and export what you want to expose

In Parent.ts

 import Child = require('Child')

 interface Parent { 
     children: Child[]
 }

 export = Parent

In Child.ts

 import Parent = require('Parent')
 
 interface Child {
     parent: Parent
 }

 export = Child

Please note, that you do not specify the extension '.ts' in require

Chore answered 27/6, 2014 at 5:59 Comment(5)
Your edit of Sept 2016 (still) generates circular dependency messages with typescript: >=2.4.2 <2.7.0 and tslint: ^5.13.1Birthroot
@BernoulliIT I removed the edit of 2016, which does not work anymore with more recent versions of typescript. The "solutions" above are still workarounds - a redesign is probably a better ideaChore
@BrunoGrieder what kind of a "redesign". For strongly typed languages cycles are normal and expected behavior!Maurreen
The VSCode plugin 'Unused exports' also doesn't see d.ts as a circular dependency :)Reedy
The Use import...require approach worked for me! TypeScript version 4.5.5 . For some reason the Use references approach also resulted in a circular dependency. And the accepted answer did not allow me to do let c: Child = new Child() (in my case, Parent and Child were classes, not interfaces).Henpeck
D
55

I also faced with the similar situation.

I could resolve by using import type.

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

Demetrademetre answered 17/12, 2020 at 9:26 Comment(2)
This is exactly what I needed, thank you so much!Elisabeth
A very elegant and quick fix. I try to always keep up with TS but missed this TS3.8 feature. Doesn't help that the description downplays its usefulness: This feature is something most users may never have to think about; however, if you’ve hit issues under isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.Carbon
C
22

Two solutions below. I prefer the latter since it offers clean interfacing with Node JS modules, but unfortunately my IDE doesn't (yet) like it as much as I do...

Use references

Create a definitions.d.ts file that will only contain the references to your classes/interfaces

/// <reference path="Parent.ts" />
/// <reference path="Child.ts" />

In Parent.ts and Child.ts, point to a single reference, the definitions.d.ts file

/// <reference path="definitions.d.ts" />

Use import...require

pass the --module commonjs flag to tsc then import what you require and export what you want to expose

In Parent.ts

 import Child = require('Child')

 interface Parent { 
     children: Child[]
 }

 export = Parent

In Child.ts

 import Parent = require('Parent')
 
 interface Child {
     parent: Parent
 }

 export = Child

Please note, that you do not specify the extension '.ts' in require

Chore answered 27/6, 2014 at 5:59 Comment(5)
Your edit of Sept 2016 (still) generates circular dependency messages with typescript: >=2.4.2 <2.7.0 and tslint: ^5.13.1Birthroot
@BernoulliIT I removed the edit of 2016, which does not work anymore with more recent versions of typescript. The "solutions" above are still workarounds - a redesign is probably a better ideaChore
@BrunoGrieder what kind of a "redesign". For strongly typed languages cycles are normal and expected behavior!Maurreen
The VSCode plugin 'Unused exports' also doesn't see d.ts as a circular dependency :)Reedy
The Use import...require approach worked for me! TypeScript version 4.5.5 . For some reason the Use references approach also resulted in a circular dependency. And the accepted answer did not allow me to do let c: Child = new Child() (in my case, Parent and Child were classes, not interfaces).Henpeck
O
6

I have about 10 ts files , in a Circular-Dependency-Hell .

The common methods can't help me any more , because the dependencies relation between 10 files is to complex.

At finally , I solved it. Using following 2 methods :

  1. Install repo ———— "circular-dependency-plugin": "5.0.2"

    This repo will helps me to find the place where circular occurs.

  2. Using a designed internal.ts , to manage my import & export

    I tried the method of this article :

    How to fix nasty circular dependency issues once and for all in JavaScript & TypeScript

    This amazing article tells me to create internal.ts .

    and using like export * form 'file-A' ; export * from 'file-B' to manage my circular dependencies.

    It works very well when I use dependencies related to my 10 files, like this import classA from '../internal.ts'.

————————————————————————————————————

If the above method has no effect on you, I found another general solution:

Use

const File_Promise = import ('yourFilePath')" to import other file or module .

when you need to use this one, use

File_Promise.then (file => { file.xxx(file.yyy) }) , just like using Promise syntax. `

This will break the Circular-Dep Chain !

If i am you , I will continue this action until NO ERROR Reported by "circular-dependency-plugin".

————————————————————————————————————

Hope to help YOU !

Occult answered 29/3, 2019 at 9:18 Comment(2)
I apply the same method but still im getting depencies warning like that src/app/shared/entities/internal.ts > src/app/shared/entities/calender-event.entity.ts src/app/shared/entities/internal.ts > src/app/shared/entities/contact.entity.ts 6) src/app/shared/entities/internal.ts > src/app/shared/entities/email-template.entity.ts 7) src/app/shared/entities/internal.ts > src/app/shared/entities/goal.entity.ts @OccultCuda
@Cuda I found a general solution. Use "const File_Promise = import ('yourFilePath')", when you need to use the function of the original File, use File_Promise.then (file => {}), just like using Promise syntax.Occult

© 2022 - 2024 — McMap. All rights reserved.