Typedoc: How should declaration merging be handled by TypeDoc?

Created on 23 Mar 2020  路  10Comments  路  Source: TypeStrong/typedoc

@Gerrit0 I'm a huge fan of the naming adjustments for namespaces but have another nuance that might be interesting to consider:

  • Exported symbols that are both namespaces and interfaces

Suppose you want to define an RPC Transport interface, you might have an interface like:

/**
 * The interface for rpc-compatible transports.
 */
interface Transport {
    /**
     * Dispose of this transport and free any allocated resources.
     */
    dispose(): void;
    /**
     * Register a RPC message listener with the transport
     *
     * @param handler A handler function to be called with each RPC message received from a peer
     */
    onMessage(handler: (msg: unknown[]) => void): {
        dispose(): void;
    };
    /**
     * Send an RPC message to the peer over this transport
     *
     * @param msg The array-encoded message that should be sent to the peer over the transport
     * @param transfer An optional array of objects that should be marked as transferrable when the transport supports it
     */
    sendMessage(msg: unknown[], transfer?: unknown[]): void;
}

However, your library already provides ready-made factories for common Transports, so you throw those in a namespace sharing the name with the interface:

declare namespace Transport {
    function fromNodeMessagePort(port: import("worker_threads").MessagePort): Transport;
    function fromDomWorker(worker: Worker): Transport;
    function fromDomMessagePort(port: MessagePort): Transport;
}

Now, typedoc will consider Transport a namespace that happens to have both functions and methods and will totally drop the fact that there is an exported interface:

image

I hope that this is something that you might consider addressing as well! 鉂わ笍

_Originally posted by @ggoodman in https://github.com/TypeStrong/typedoc/pull/1184#issuecomment-593464125_

discussion

All 10 comments

Pulled this out into its own issue as I think there are several issues we need to consider.

  1. TypeScript differentiates between type and value space. Right now, TypeDoc tries to do something similar with whether or not something is static when merging... but this doesn't really work.
  2. Comment links need to be able to specify whether they are linking to the type or value side of things. Should {@link Foo} link to the interface Foo or the namespace of the same name?

Issue 1 raises another problem. Should TypeDoc merge everything that TypeScript does? I think the answer to this is no. I think there is value in differentiating between classes & namespaces. (Backpedaling on what I said here https://github.com/TypeStrong/typedoc/pull/1184#issuecomment-599248647) This is valid TS, and b is currently shown as a static member of the class Foo by TypeDoc... but it really isn't.

class Foo {
    a = 1
}
namespace Foo {
    export const b = 1
}

Likewise, merging a namespace and enum should result in two reflections.

I think the only merging that should happen is between classes and interfaces (of course, namespace + namespace or interface + interface should still be merged), since merging there can be used to tell TS about metaprogramming additions.

Cross linking: #472, #615, #738

Maybe a solution for ambiguous links is to introduce new 'disambiguation nodes' (think of Wikipedia disambiguation pages) that such links would point to.

My first reaction to that was "oh that's cool", followed immediately by "that shouldn't be necessary, any link that is ambiguous is just asking people to use the wrong type." I'd rather adopt a linking strategy that always results in an unambiguous result, or errors if a result is ambiguous (yet again coming back to - we need to adopt TSDoc or some similar standard rather than just mashing comments with regex to get something that mostly works)

Fair call. Another 'clever hack' might be to kick the can down the road and have the different themes provide the disambiguation; you could indicate to the theme that for a given @link there is ambiguity and let it 'figure it out'. Maybe it will present a pop-up for choosing or link to a footnote with the different options? 馃し鈥嶁檪

I think that's it for my supply of 'clever hacks'! Hope some of this helps ideation though!

I seem to be stuck on this.
Here's a simplified example of what I'm facing.

We have a library which exposes itself in the global namespace using a tree structure. There are classes (Library.Chart) and Namespaces (Library.Chart) with the same name. The namespaces can include more classes (Library.Chart.State or enums/types/functions.
TypeDoc creates only a namespace for this case. The class (Library.Chart) is completely lost in the transition. Properties become variables, methods functions.

What I exped was to have both the namespace and the class.

I'm also seeing some other strange behaviour related to inheritance, where _namespace_ BarChart (the _class_ extends Chart) suddenly has State as class in its namespace. Not sure if it relates to this problem or if it is a new one in itself.

https://github.com/TypeStrong/typedoc/issues/1244#issuecomment-602294243 namespaces and classes should be treated differently and not merged, as you said.

Is there any current workaround for this? Or any idea when this will be fixed?

Your expectation is the behavior TypeDoc will have eventually. I consider it an important part of library mode so will be making sure it is done as a part of that. That is, this should be fixed in the next minor version of TypeDoc (unless something happens that requires a bump there before library mode is done... again)

Unfortunately, I don't have a workaround for you today, or a date for when the next minor version will be. It's looking like I should be able to spend a few hours tomorrow working on it, but then chances are I won't be able to touch it again for a couple weeks... Too many priorities!

Another issue here that just came up. What about functions?

function Foo(arg = Foo.defaultArg) { // Ew... but people do this.
  return 1;
}
namespace Foo {
  export const defaultArg = 1
}

How do we document this? Two different elements? A namespace with signatures? A function with properties?

Unlike the namespace / interface issue, these are both value space. It is likely useful to have these in close proximity... maybe they should also be documented separately and we should add a property to reflections that make it possible to check what else merges with this?

This is valid TS, and b is currently shown as a static member of the class Foo by TypeDoc... but it really isn't.

class Foo {
    a = 1
}
namespace Foo {
    export const b = 1
}

+1 for documenting this namespace and class separately.

class Foo plus namespace Foo is a pattern that is ubiquitous in@jupyterlab core. Separate docs for each would better capture the original intent of the code (which is closer to "here's a class plus a bag of related values" than it is to "here's a class and its statics").

0.20 (release later today) makes some progress here - we no longer try to perform declaration merging (exception: class + interface)

It's still not perfect - links will always go to one of the reflections instead of the one that makes the most sense depending on if it was placed in a value... but it is a step in the right direction.

The linking problem is a separate issue, which will be resolved once we're closer to aligned with the TSDoc standard... I think this has been resolved.

Was this page helpful?
0 / 5 - 0 ratings