javascripttypescriptes6-module-loadertypescript-declarations

Are circular references ok in .d.ts files?


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?


Solution

    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