microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.02k stars 12.49k forks source link

Circular reference error happens in jsdoc but not in typescript. #46369

Open jespertheend opened 3 years ago

jespertheend commented 3 years ago

Bug Report

🔎 Search Terms

circularly references itself jsdoc ts2456

🕗 Version & Regression Information

⏯ Playground Link

Playground link (js) Playground link (ts)

💻 Code

Foo.js

/** @typedef {Object.<string, Foo>} Foo */

Foo.ts

type Foo = {
    [x: string]: Foo;
};

🙁 Actual behavior

Type alias 'Foo' circularly references itself. ts(2456) happens in Foo.js. The TypeScript equivalent works fine.

🙂 Expected behavior

Both Foo.ts and Foo.js work without errors.

Related issues

I found some related issues but they are either closed or use a different example.

39372 - Closed (fixed)

45641 - Seems very similar but uses Array<> and typescript rather than jsdoc. I'm not sure if the root cause is the same so this might be a duplicate.

junaga commented 2 years ago

I have the same issue.

cedx commented 2 years ago

Same here (using TS 4.6.3).

jespertheend commented 2 years ago

The workaround is to use TypeScript in the JSDoc comment, rather than Object.<string, Foo> notation:

/**
 * @typedef {{
 *  [x: string]: Foo
 * }} Foo
 */

(playground)

cedx commented 2 years ago

@jespertheend It doesn't work every time.

// TypeScript: OK
export type Json = null | boolean | number | string | Json[] | {[property: string]: Json};
// JavaScript: error 2456
/**
 * @typedef {null | boolean | number | string | Json[] | {[property: string]: Json}} Json
 */
jespertheend commented 2 years ago

Seems like that's because Json[] is also in there. Another way to work around this is to add an extra type just for the array:

/**
 * @typedef {null | boolean | number | string | JsonArray | {[property: string]: Json}} Json
 */

/** @typedef {Json[]} JsonArray */

(playground)

kungfooman commented 1 year ago

I wrote a little template typedef to help creating circular references:

/**
 * @typedef {T | CircularArray<T> | CircularObject<T>} Circular
 * @template T
 */
/**
 * @typedef {Circular<T>[]} CircularArray
 * @template T
 */
/**
 * @typedef {{[key: string]: Circular<T>}} CircularObject
 * @template T
 */
/**
 * @typedef {Circular<number | string>} NumberStringObject
 */
/** @type {NumberStringObject} */
const x = {
    y: [1, {z: [1, 2, 3]}]
}

(playground)

Another example, lets say you have a type like this:

/**
 * @typedef {{destroy: Function} | HTMLElement | Destroyable[] | undefined | null} Destroyable
 */

Error: image

Solution:

/**
 * @typedef {T | CircularArray<T>} Circular
 * @template T
 */
/**
 * @typedef {Circular<T>[]} CircularArray
 * @template T
 */
/**
 * @typedef {{destroy: Function} | HTMLElement | undefined | null} DestroyableTypes
 */
/**
 * @typedef {Circular<DestroyableTypes>} Destroyable
 */
/**
 * @param {Destroyable} o
 */
export function destroy(o) {
  if (!o) {
    return;
  }
  // Order of most to least specific types (every array is an object, but not vice versa)
  if (o instanceof Array) {
    // Destroy an array by destroying every component recursively and setting length to 0
    o.forEach(destroy);
    o.length = 0;
  } else if (o instanceof HTMLElement) {
    // TODO: maybe remove() with Removeable instead?
    o.remove();
  } else if (o instanceof Object && typeof o.destroy === "function") {
    o.destroy();
  } else {
    console.warn('destroy has no function for type', typeof o);
  }
}
const data = [{
  destroy() {
    console.log("oh no");
  }
}, {
  destroy() {
    console.log("aaaahhh");
  }
}, [
  {
    topkek() {
      console.log("grrraaaaaaaaaahh"); // console warns> destroy has no function for type object
    }
  }, {
    destroy() {
      console.log("*dies in silence*");
    }
  }
]];
destroy(data);

(playground)