How Docs-as-Code and TypeDoc Revolutionize TypeScript Library Documentation
This article explores the motivation, design, and implementation of using Docs-as-Code with TypeDoc to generate and maintain documentation for TypeScript utility libraries, covering annotation conventions, plugin architecture, code organization, flag handling, event mechanisms, reflection generation, and practical plugin customizations.
1. Introduction & Background
Background: As the team increasingly uses TypeScript for projects that require continuous maintenance and iteration, documentation for internal utility or component libraries becomes essential. Initially, markdown was used, but as the project grew and multiple contributors joined, documentation standards diverged, updates lagged, and the docs fell out of sync with the code.
Thoughts: Extensive practice revealed that most documentation is closely related to TypeScript code comments, leading to the adoption of the Docs-as-Code concept. If a TypeScript project follows a unified comment convention and can generate documentation from those comments, integrating this capability into CI/CD would greatly improve development efficiency and iteration speed.
Attempt: We experimented with generating documentation for frequently used utils. The project adopts the tsdoc comment standard and uses the community TypeDoc tool to generate docs, integrating the generation step into the CI/CD pipeline. The generated documentation for the onex-utils project is shown below.
Deep Dive: With ongoing project iterations, the value of TypeDoc‑generated documentation became increasingly evident. To meet more advanced documentation needs, we studied TypeDoc’s source code, extracted its core execution and design ideas, and proposed plugin‑based customizations for onex-utils. The following sections introduce TypeDoc’s principles and showcase two plugin examples.
2. Execution Logic
Read options – determine which plugins should be loaded.
Load plugins – if the --plugin flag is used, load only specified plugins; otherwise load all npm plugins containing the typedocplugin keyword.
Read options again – pick up plugin‑specific options.
Convert input files into models (reflections) under src/lib/models.
Resolve the models – handle links that may not exist when the source model is generated.
Output the models – serialize to HTML and/or JSON.
3. Project File Tree
| |____lib
| | |____converter # Based on rules convert files
| | |____serialization # JSON output logic
| | |____output # HTML output logic
| | |____utils # Utility types
| | |____models # File models describing annotation classes
| | |____ts-internal.ts
| | |____application.ts # Bootstrap file for application startup4. Core Idea
The core execution flow is outlined in section 2. Below we discuss the code‑design highlights discovered by reading the source.
1) Code Organization
The project composes components via inheritance and composition, forming a tree where each node represents a functionality. A parent component can add child components via this.addComponent, and specify child types with childClass. This structure aligns with the open‑closed principle and enables flexible navigation between nodes.
1. Introduction
Parent (host) component can add child component instances using this.addComponent.
Parent can define the child type via childClass; the decorator ensures that subclasses of ChildableComponent are instantiated together with the host.
2. Base Class Implementation
Abstract base class – child component
/**
* application type needs to be defined as the top‑level type, temporarily string
*/
export interface ComponentHost {
readonly application: string;
}
const TOP_APPLICATION = Symbol();
export abstract class AbstractComponent<O extends ComponentHost> implements ComponentHost {
private _componentOwner: O | typeof TOP_APPLICATION;
componentName!: string;
constructor(owner: O | typeof TOP_APPLICATION) {
this._componentOwner = owner;
this.initialize();
}
get application() {
if (this._componentOwner === TOP_APPLICATION) {
return this as unknown as string;
}
return this._componentOwner.application;
}
initialize() {
/**
* Initialization logic executed when a child component is instantiated
*/
}
}Abstract base class – host component implementation (may contain child component types)
export type Component = AbstractComponent<ComponentHost>;
export interface ComponentClass<T extends Component, O extends ComponentHost = ComponentHost> extends Function {
new (owner: O): T;
}
export abstract class ChildableComponent<O extends ComponentHost, C extends Component> extends AbstractComponent<O> {
_componentChildren?: { [name: string]: C };
_defaultComponents!: { [name: string]: ComponentClass<C> };
constructor(owner: O | typeof TOP_APPLICATION) {
super(owner);
// Instantiate default components and store them in _defaultComponents
this.addComponent();
}
addComponent() {}
getComponent(name: string) {
return (this._componentChildren || {})[name];
}
}3. Usage
Create the top‑level root node, which adds a Child component (host type)
import { AbstractComponent, ChildableComponent, Component, DUMMY_APPLICATION_OWNER } from './component';
/**
* Top‑level application; the host is itself
*/
@Component({
name: 'application',
internal: true,
})
class Application extends ChildableComponent<Application, AbstractComponent<Application>> {
constructor() {
super(DUMMY_APPLICATION_OWNER);
// Add child type
this.addComponent<Child>('child', Child);
}
}Second‑level host component implementation
import { AbstractComponent, ChildableComponent, Component, DUMMY_APPLICATION_OWNER } from './component';
/**
* Abstract base class for third‑level child components
*/
abstract class ChildClass extends AbstractComponent<Child> {}
/**
* Second‑level child container
*
* @remarks
* 1. This container is a child of Application
* 2. It serves as the host for ChildClass
*/
@Component({
name: 'child',
internal: true,
childClass: ChildClass,
})
class Child extends ChildableComponent<Application, ChildClass> {}Third‑level child component – implementation and registration via decorator
/**
* Attach a third‑level child component, automatically injected into the second‑level container
*/
@Component({
name: 'testChild',
})
class TestChild extends ChildClass {
initialize() {
console.log('Instantiate subclass');
}
}4. Personal Understanding
Organizing code in this tree‑like fashion makes each part of the project easy to manage, especially when dealing with plugins. Child components can access any other component in the application through the tree.
Parameters can be bound using the BindOptions decorator, which quickly links options to a specific layer; the source of BindOptions is shown in the code excerpts.
2) Plugin Mechanism
1. Introduction
The typeDoc plugin mechanism relies on event listeners. During execution, key steps emit events that plugins can listen to. Because TypeDoc structures the project as a tree, events can be listened to and triggered based on tree dimensions.
2. Core Mechanism
Plugin injection via code decorators: decorators inject plugins directly, and plugins typically listen to events on the host object.
Global configuration injection: a function receives the top‑level application instance, allowing listeners to be attached anywhere in the code.
During plugin initialization, the initialize method registers listeners on the Application or host component, reacting to emitted events.
Event callbacks receive two parts: the component hierarchy (host/child) and the corresponding Converter processing context.
3) Flags
1. Introduction
Q: Why use powers of two for flag values? A: Bitwise operations enable fast checks such as remove, hasAll, hasAny.
2. Logic
// T & {} reduces inference priority
export function removeFlag<T extends number>(flag: T, remove: T & {}): T {
return ((flag ^ remove) & flag) as T;
}
export function hasAllFlags(flags: number, check: number): boolean {
return (flags & check) === check;
}
export function hasAnyFlag(flags: number, check: number): boolean {
return (flags & check) !== 0;
}
// Combine into a common flag set
const commonFlags = ts.SymbolFlags.Transient |
ts.SymbolFlags.Assignment |
ts.SymbolFlags.Optional |
ts.SymbolFlags.Prototype;4) Event Mechanism Implementation
1. Idea
The overall idea mirrors a traditional event queue, with the highlight being abstracted logic encapsulation.
2. Implementation
// Event queue definition
interface EventHandlers {
[name: string]: EventHandler[];
}
// Single event handler definition
interface EventHandler {
callback: EventCallback; // event callback
context: any; // listener context
ctx: any;
listening: EventListener; // trigger context
priority: number; // event priority
}
interface EventListener {
obj: any;
objId: string;
id: string;
listeningTo: EventListeners;
count: number;
}
// Iterator that determines how to process events after they fire
interface EventIteratee<T, U> {
(events: U, name: string, callback: Function | undefined, options: T): U;
}3. Personal Understanding
The whole event handling system is designed to work with the component hierarchy; each child component has its own id, and events are only triggered on the queue bound to that instance.
Execution is abstracted: implementing an EventIteratee is enough to define how events are iterated.
5) Reflection Object Generation
Build the context object.
Perform the first split based on the entry file.
Locate exported symbols in each file.
For each exported symbol, analyze its type and type parameters using the TypeScript Compiler API.
Convert each exported symbol's declarations into Reflection objects.
6) Reflection Output
Through the structured Reflection data, we can convert Reflection objects into HTML or serialized JSON.
1. Serialized Object Output (strongly dependent on Reflection structure)
Determine the type: extract certain properties from Reflection to form a custom serialized object.
Output conversion: starting from ProjectReflection, traverse and transform the object.
2. HTML Output (Theme concept)
Use the Reflection structure to decide the output type and URL.
Pass the Reflection object to an HBS template for rendering.
After the Render file runs, generate the final Content and write it to the output folder, producing the complete documentation.
5. Code Excerpts
1) Parameter Binding Decorator
/**
* Binds an option to the given property. Does not register the option.
*
* @since v0.16.3
*/
export function BindOption<K extends keyof TypeDocOptionMap>(name: K):
<IK extends PropertyKey>(
target: ({ application: Application } | { options: Options }) & { [K2 in IK]: TypeDocOptionValues[K] },
key: IK
) => void;
/**
* Binds an option to the given property. Does not register the option.
* @since v0.16.3
*
* @privateRemarks
* This overload is intended for plugin use only with looser type checks. Do not use internally.
*/
export function BindOption(name: NeverIfInternal<string>): (
target: { application: Application } | { options: Options },
key: PropertyKey
) => void;
export function BindOption(name: string) {
return function (target: { application: Application } | { options: Options }, key: PropertyKey) {
Object.defineProperty(target, key, {
get(this: { application: Application } | { options: Options }) {
if ("options" in this) {
return this.options.getValue(name as keyof TypeDocOptions);
} else {
return this.application.options.getValue(name as keyof TypeDocOptions);
}
},
enumerable: true,
configurable: true,
});
};
}2) Flag Operations
// T & {} reduces inference priority
export function removeFlag<T extends number>(flag: T, remove: T & {}): T {
return ((flag ^ remove) & flag) as T;
}
export function hasAllFlags(flags: number, check: number): boolean {
return (flags & check) === check;
}
export function hasAnyFlag(flags: number, check: number): boolean {
return (flags & check) !== 0;
}
// Combine into a common flag set
const commonFlags = ts.SymbolFlags.Transient |
ts.SymbolFlags.Assignment |
ts.SymbolFlags.Optional |
ts.SymbolFlags.Prototype;6. Plugin Customization
1) Short Name Implementation
Description: The generated documentation pages are based on the file directory structure. For the onex-utils project, the full path adds little value, so we want to keep only the essential parts (e.g., src/utils → group name and function name).
Idea: Replace the src/utils/ prefix in the Reflection name fields during rendering.
Implementation:
// Listen to the render event of the application
export const load = (that: Application) => {
that.listenTo(that.application.renderer, {
[PageEvent.BEGIN]: changeAlias,
});
};
/**
* When reflection conversion begins, modify the alias for global pages
*/
function changeAlias(page: PageEvent) {
page?.model?.groups?.forEach((element: any) => {
if (element.categories) {
element?.categories?.children?.forEach((cate: any) => {
cate.name = cate.name.replace('src/utils/', '');
});
return;
}
element.children.forEach((ele: any) => {
ele.name = ele.name.replace('src/utils/', '');
});
});
}2) Version Record
Description: onex-utils is a utility library that evolves with new features. Each feature should have an associated version, and this version information should be rendered into the generated documentation.
Idea: Store version records in a static file. When a new release is made, fetch the previous version data via HTTPS, update the file, and upload it again.
Implementation:
export const load = (that: Application) => {
that.listenTo(that.application.renderer, {
[RendererEvent.BEGIN]: getVersionMap, // Retrieve version map before rendering
[RendererEvent.END]: saveVersionMap, // Save version map after rendering
[PageEvent.BEGIN]: updateVersionMap, // Update version map during page rendering
});
};
// The detailed implementation is omitted for brevity.7. References
TypeScript Compiler API: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
TypeDoc source code
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
