Are circular references ok in .d.ts files?

269 Views Asked by At

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?

1

There are 1 best solutions below

4
Dimava On BEST ANSWER
  1. 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.

  2. 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
  1. Runtime circular references in TS code are OK as well - as long as they instantiated before access. This may be not supported by 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')

  1. If you have a lot of types, and bundle the code or whatever, you may use https://www.npmjs.com/package/@microsoft/api-extractor to rollup the types into a sindle d.ts file