How to solve Circular Dependencies when using classes as Types in Typescript?
Asked Answered
G

3

13

How can i use a typing in typescript that does not run into circular dependency errors?

It seems that the circular dependency error occurs, even though the imports should be removed when the code compiles to valid JS. Is that a bug?

user-model.ts

import { Post } from '../post-model'

export class User {
  Posts: Post[];
}

post-model.ts

import { User } from '../user-model'

export class Post {
  User: User;
}

I've heard about two possible solutions that both don't satisfy me.

One is, to create a new interface that matches the class: Circular dependency caused by importing typescript type

And I've read something in the docs of typegraphql: https://typegraphql.com/docs/types-and-fields.html

There they say:

Why use function syntax and not a simple { type: Rate } config object? Because, by using function syntax we solve the problem of circular dependencies (e.g. Post <--> User), so it was adopted as a convention. You can use the shorthand syntax @Field(() => Rate) if you want to save some keystrokes but it might be less readable for others.

I also didn't find any option to disable the circular dependency warning in typescript.

I'm working in Nrwl/Angular 9.x

Genoa answered 16/4, 2020 at 20:5 Comment(5)
this isn't valid. you can't have a circular type like this. it's not logically sound to say a user has a list of posts, each of which has a user, each of which has a list of posts, each of which has a user, each of which has a list of posts .... and so on and so on ad infinitum. it may be true but it's not something you can code withConlin
But it IS valid. Which is why it will work if you put both classes in the same file. The loop problem here is not about the classes, but about the imports. The imports tell TS which files should be compiled first, thus creating this problem.Thomsen
@Conlin Sorry, but it is completely valid to have that circular relationship. There are heaps of ORMs which use this.Optics
thanks everyone. This is a confusing problem.. In my opinion the Javascript could be completely valid but it seems Typescript needs to find the right order to compile those files. I guess it would be nice if the typescript compiler could differentiate between imports that are used for typings, and imports that are used as an actual value. That way, my code would not have a circular dependency.Genoa
The actual issue Typescript is trying to solve is after the transcompilation occurs, which file contents are to be physically placed first in the resulting Javascript. In your case, everything is encapsulated in a class, but imagine if there was global javascript references within that file as well. The ordering of this would matter. So it throws an error about the circular reference to circumvent this.Optics
I
16

Another solution that does not use interfaces is to use type only imports.

user-model.ts

import type { Post } from './post-model'

export class User {
  Posts: Post[];
}

post-model.ts

import type { User } from './user-model'

export class Post {
  User: User;
}

These imports get completely removed upon compilation and serve only for the purpose of type checking - that means you can't use them as a value (you can't do new Post() with a type-only import for example).

I think this approach is cleaner and more DRY than the alternative that is creating a separate file with interfaces only for the sake of type checking.

Isogamy answered 14/7, 2021 at 14:41 Comment(2)
Awsome! That‘s what I was looking for! Is it a new feature?Genoa
@Genoa seems it was introduced in typescript 3.8 which came out on February 20, 2020, so it had been already out for 2 months when you asked the original question.Isogamy
O
6

Using interfaces is the best option here. You can make a .d.ts for each and then import that instead.

user.d.ts

export interface IUser {
  Posts: IPost[];
}

post.d.ts

export interface IPost {
  User: IUser;
}

And then...

import { IPost, IUser } from './post.d'

export class User implements IUser {
  Posts: IPost[];
}
Optics answered 16/4, 2020 at 20:22 Comment(2)
This answer should have more upvotes. I just used it to fix a heap of errors in my project. cool pattern, just requires a little setup to implement.Horsewoman
To me this approach seems to be really hard to maintain if you're working with bigger classes and more interdependent models. You always need to keep your interfaces up to date and implement methods and return types. Interfaces do not cause circular dependencies but why do Classes, if they are just used as a type like an interface.Genoa
H
1

You can do this only if you put them in the same file

export class User {
  Posts: Post[];
}

export class Post {
  User: User;
}

or if you write them types

user-post.types.ts

export interface User {
  Posts: Post[];
}

export interface Post {
  User: User;
}

user-model.ts

import { Post } from '../user-post.types'

export class User {
  Posts: Post[];
}

post-model.ts

import { User } from '../user-post.types'

export class Post {
  User: User;
}
Helen answered 16/4, 2020 at 20:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.