TypeStrong / typedoc

Documentation generator for TypeScript projects.
https://typedoc.org
Apache License 2.0
7.67k stars 692 forks source link

Better documentation for dynamically-typed event buses #2709

Open Oblarg opened 1 week ago

Oblarg commented 1 week ago

Search Terms

events, type inference, discriminated unions, mapped types

Problem

Documenting a typed event bus is difficult in TypeDoc. If our event binding method is genericized to dynamically type the payload argument in a correct manner, at best we get a long list of string keys for our events, but no real way to view the payloads matching each event key:

image

A simple reproduction case (with several levels of type complexity) can be found in this typescript playground.

Suggested Solution

There should be some way to either render inline or link to an appropriate expansion/flattening of the corresponding event payload types alongside the string key values.

Gerrit0 commented 1 day ago

After a few hours looking into this... producing the type of documentation you're looking for is ridiculously painful. In an ideal world, Topic.on in the last example would be documented as if it was written:

function topic(eventType: "bar", (event: Bar & { topic: "topic" }) => void): void;
function topic(eventType: "baz", (event: Baz & { topic: "topic" }) => void): void;
// ...

... or in some cases, inlining even further as:

function topic(eventType: "bar", (event: { type: "bar", topic: "topic" }) => void): void;
function topic(eventType: "baz", (event: { type: "baz", topic: "topic" }) => void): void;
// ...

Note: This assumes that on should be declared as the following, with a generic constraint that can be used to enumerate the possible signatures.

export function on<EventType extends Topic.Event["type"]>(
    eventType: EventType,
    handler: Handler<EventType, Event>,
) {}

There are a few problems with this, including:

  1. TypeScript does not export the necessary functions to be able to perform this surgery. TypeDoc needs to ask TypeScript to instantiate type aliases with each member of the type parameter constraint. (really, a plugin, there's no chance of this ever being built in, way too nasty)

    The necessary function is getTypeAliasInstantiation in the type checker. Using patch-package to add it isn't that much code.

  2. Even using the type returned by getTypeAliasInstantiation, TypeScript doesn't eagerly evaluate the alias, so TypeDoc ends up converting it as Handler<"foo", Events>. 6b9360f adds an @inline tag that can be placed on Handler to make TypeDoc do what you want (shipping in 0.27)... mostly, with the limitation that it doesn't work when TypeDoc is converting with type nodes (rather than types, they are used for functions outside classes and type aliases today, I might change it just be type aliases, don't remember why it works like that today...), it doesn't respect @inline if there are type arguments.

... all that said, https://github.com/Gerrit0/typedoc-plugin-generic-signature-overloads exists now, and produces this output, in a horrible hacky way:

image

Oblarg commented 13 hours ago

Woah! Thanks a ton for the work here, this is awesome. Not too surprised the implementation is a pain, but the output here is great.