When creating typescript declarations for a javascript library, is it ok to have circular imports in .d.ts files? E.g.:
file a.d.ts
import { B } from "./b";
export class A { ... }
...
file b.d.ts
import { A } from "./a";
export interface B { ... }
...
As I understand, these are only used for compilation. Let's assume there are no circular dependencies in the javascript library. So far i did not find any problems when using declarations like above in a test program. My question is, is this sort of practice ok, or considered bad design that should be avoided? I understand that the same approach would cause problems at run-time (undefined exports, etc.), but at compile-time it seems that typescript compiler is able resolve these (perhaps by merging all declarations into one module?). Or am I missing something and this practice should be avoided? If so, can you give an example when this would cause a problem when using such .d.ts files?
When you create declarations for existing JS libraries, you don't have to care if it's good or bad, just type it as it is. TS was made to cover as much JS as possible so if it works in JS it'll probably work in TS.
Circular references in types are very common, and sometimes unavoidable e.g.
// `parent` can't be just a common interface because exact typings are needed and and DRY
class A {
parent?: A | B
}
class B {
parent?: A | B
}
or circular generics
class A<T> { b?: B<T> }
class B<T> { a?: A<T> }
so they are OK. What's not OK is that you are using full imports. You should be using
import type { A } from './a'
// ^^^^ says it doesn't actually imports that in runtime
commonjs
modules tho.// e.g. used in both A and B classes
function makeAorB(x: boolean): A | B { return x ? new A() : new B() }
Example of failing instantiation would be zod TypeError: Cannot read properties of undefined (reading '_parse')