tsdoc
tsdoc copied to clipboard
RFC: Marking events using @eventClass and/or @eventProperty
We have two different API Extractor consumers who define class "events". In C# this is formalized via an event keyword. Although the TypeScript language doesn't model events, it's fairly easy to simulate.
You define an "event class" (e.g. SPEvent) that allows adding/removing handlers:
class FrameworkEvent<TEventArgs extends FrameworkEventArgs> {
public add(eventHandler: (eventArgs: TEventArgs) => void): void {
// ...
}
public remove(eventHandler: (eventArgs: TEventArgs) => void): void {
// ...
}
}
And then you expose readonly-properties (e.g. ApplicationAccessor.navigatedEvent) returning this object:
class MyClass {
public readonly navigatedEvent: FrameworkEvent<NavigatedEventArgs>;
}
And then users can attach an event handler like this:
const myClass = new MyClass();
myClass.navigatedEvent.add((eventArgs: NavigatedEventArgs) => {
console.log('Navigated to:' + eventArgs.destinationUrl);
});
The feature request is for the documentation tool to group members like navigatedEvent under an "Events" heading, instead of the usual "Properties" headings.
Proposed Solutions
We're proposing three approaches to support this:
1. Mark up the class: Define a TSDoc tag @eventClass for the class:
/**
* This is the class used to represent API events.
* @eventClass
*/
class FrameworkEvent<TEventArgs extends FrameworkEventArgs> {
public add(eventHandler: (eventArgs: TEventArgs) => void): void {
// ...
}
public remove(eventHandler: (eventArgs: TEventArgs) => void): void {
// ...
}
}
Based on this, ideally a tool like API Extractor would recognize any readonly property that returns FrameworkEvent, and automatically classify that as an “event” in the documentation.
2. Mark up each event property: Define a TSDoc tag @eventProperty which needs to be applied to each individual property. This more work for API owners, but it has the benefit of being more obvious to API users who will see these comments in the *.d.ts files. Also it can handle edge cases where something has a signature that looks like an event, but wasn't intended to be. Example:
class MyClass {
/**
* This event is fired whenever the application navigates to a new page.
* @eventProperty
*/
public readonly navigatedEvent: FrameworkEvent<NavigatedEventArgs>;
}
3. Both: If we supported both tags, then you would manually mark individual properties as in #2, but a tool like API Extractor can use the information from #1 to find places where @eventProperty is missing or improperly added, and issue appropriate warnings in "strict mode".
NOTE: JSDoc already defines an @event tag with different meaning, so the proposed tags were chosen to avoid conflicts/confusion.
@mpasarin @dend @AlexJerabek FYI
I'm down for option 2, since it mirrors reality the closest.
I'd say also consider the eventbus ala pub / sub angle too. Here is a recent random Microsoft doc on it...
I have a slight preference for #2, but I'm fine with #1 too.
@eventProperty supports one style of events, and while the type of event it describes is common in the C# land, it does not capture all common patterns in JS/TS.
Node provides the EventEmitter class. This class lets users emit events by a string (or potentially symbol) name. This can be made more strongly typed (example), but TSDoc doesn't provide any standard way to document these events.
- JSDoc uses the
@eventtag to describe what is emitted under a certain name - TypeDoc uses the
@eventtag to group properties together under an "events" group, but doesn't provide any nice way to specify the type of the object(s) emitted.
How should we document this code?
class Foo extends EventEmitter {
static readonly START = 'start' // doc comment here?
static readonly END = 'end'
doSomething(arg: number) {
this.emit(Foo.START, 'doSomething', arg)
// do work
this.emit(Foo.END, 'doSomething', arg)
return work
}
}
🤔 Is there maybe a clever way to use the TypeScript type system to describe the events that can be fired?
Do you have an example of a real API documentation website (generated by JSDoc or TypeDoc) that shows how the @event tag is processed?
Sorry for the delay here, TypeDoc uses @event to place members tagged with it under a separate heading.

https://typedoc.org/api/classes/converter.html
Yes, there's a clever way to describe the events that can be fired, linked under example in my previous reply. The trick is to make the class generic on an event map.
In the apps I'm working on we use some sort of publisher pattern (not really a pubsub but kind of : @qphi/publisher-subscriber) so declaring the events on the classes themself doesn't work. Instead we use enums (or plain const: Record<string, string>) being able to mark an enum member as an event of a given class (or the whole enum maybe) would be handy
In our product code we dispatch custom custom DOM events from our custom DOM elements. And of course standard DOM elements also dispatch standard DOM events in the same way which are currently not yet documented by the lib.dom.d.ts documentation.
This could potentially also be very useful for supporting more advanced linting to check whether the type of an event listener is correct for the related element(s). All TypeScript currently supports there now AFAIK is the GlobalEventHandlersEventMap and I understand that events can bubble so that makes some sense. But in many cases events don't bubble and it would be great if we could enable strict type checking there. I guess I could also override the types of addEventListener() and removeEventListener(), but that would be cumbersome and still not reliable cover all uses.
I currently declare custom events like so, p.e. in LiveryErrorEvent.ts:
declare global {
interface GlobalEventHandlersEventMap {
'livery-error': LiveryErrorEvent;
}
}
/**
* Dispatched when an error occurs.
*
* @group Events
*/
export class LiveryErrorEvent extends Event {
/**
* Event type.
*/
static type = 'livery-error' as const;
constructor(
/**
* Error that occurred.
*/
readonly error: Error,
init?: EventInit,
) {
super(LiveryErrorEvent.type, init);
}
}
Then I document our class LiveryPlayer extends LitElement (from Lit which in turn extends HTMLElement) with a list of:
/**
* Element defined as `<livery-player>` which can be used to play a Livery video stream as specified by
* {@link streamId}.
* ..
* This can dispatch the following events:
* - {@link LiveryErrorEvent | livery-error} - Dispatched when an error occurred
* ..
* @group Elements
*/
As that looks better at tsdocs.dev then using the old JSDoc @fires syntax. E.g. see: https://tsdocs.dev/docs/@liveryvideo/player/7.9.0-beta.2/classes/LiveryPlayer.html
But what I'd really like to have standard support for is something specifically suitable for DOM events, let's call it: @dispatches, p.e:
/**
* @dispatches {LiveryErrorEvent} livery-error - Dispatched when an error occurred
*/
So similar in syntax to @param but linking to the related event class and specifying the event type value that it is dispatched with and can be listened to and an event description.
This would not only facilitate writing such documentation, but theoretically also enable checking it for correctness and refactoring etc. It could be used by standard and custom elements to declare which events they can dispatch in general. But also by element methods to point to events that could be dispatched as a result of calling that method. And perhaps also by element properties to point to the events that are dispatched when the property value is changed.