Writing docs that can't go stale
Document definitions with ordinary JSDoc. The parts of a doc that name code are treated as code: they're resolved to hashes when the definition is added, so they follow renames and updates, and examples are typechecked and run. (Links between pages are the exception: kept as written, and checked when a page is saved.)
/**
* The sum of the {@link square}s of some numbers.
*
* {@signature square}
*
* ```ts
* sumSquares([1, 2, 3])
* ```
*/
export const sumSquares = (xs: number[]): number => xs.map(square).reduce((a, b) => a + b, 0);
tcm docs lib.sumSquares shows it rendered, with the example evaluated:
The sum of the `square`s of some numbers. `square : (x: number) => number` ```ts sumSquares([1, 2, 3]) ``` ```text ⧩ 14 ```
What a doc can contain
{@link square},{@link lib.square}- a link, shown as the definition's current name (its shortest unambiguous suffix, like
square) {@link square | the square},{@link square the square}- a link with its own text
{@linkcode square}- a link shown as code
{@signature square}- the definition's name and type
{@source square}- the definition's source, as a code block
- a
```tsblock - an example, typechecked and run top to bottom (see below). Other code blocks are just text
@param,@returns,@throws,@see,@example,@deprecated- JSDoc's block tags: a Parameters list, headed paragraphs, and a warning for
@deprecated @since 2.0.0,@category mapping,@stability stable,@version,@author;@internal,@experimental,@alpha,@beta,@public- facts about the definition: one line at the end of its doc (Since 2.0.0 · Category: mapping · Stability: stable), in that order, and chips under its name in the web UI
Everything else is Markdown: headings, lists, tables, emphasis.
Examples run every statement in order. Each top-level expression's value is shown after the block (strings as
their text), so write steps that matter only for their effect as consts. An example can use what the
definition's file imports: other definitions, packages, Node builtins, the server library (server code runs in
memory, as in tests).
How it stays current
Names resolve when the definition is added. A link's name, and every name an example uses, is looked up where the definition is (its imports and the definitions beside it), or as a codebase path (
lib.square), and stored by hash. A link that doesn't resolve is a warning attcm add. An unresolved name in an example is left as written (it may be a global, likeMath).Renames are free. The doc shows the definition's current name, in prose, signatures and examples.
Updates carry through. A doc refers to definitions by hash and follows
tcm update's patches, so it always shows and runs the newest version. A dependent re-derived byupdatekeeps its doc.Examples run against current code, each as a small module importing what it uses. Values are cached by the whole example (every statement's hash), so a change to any step, or anything a step uses, runs it again.
Broken examples are reported. After
tcm addandtcm update, examples in the docs just saved, and in any doc naming something replaced, must still typecheck. Those that don't are listed:! lib.show: doc example: Argument of type 'number' is not assignable to parameter of type 'string'.
The rendered doc shows the example with the reason.
tcm importdoesn't check docs: a library's examples are usually written for readers, not to typecheck on their own.
Docs aren't part of a definition's hash, so editing a doc doesn't change the code's identity.
Namespace READMEs
A definition named README is its namespace's front page. A type alias carries a doc and nothing else:
/**
* # Polls
*
* A live polling app. Start with {@link main}.
*/
export type README = never;
The editor shows it at the top of the namespace's page (editors).
Where docs are shown
tcm docs <name>in the terminal (Markdown, examples evaluated);- editor hovers (rendered, examples shown but not run);
- definition and namespace pages in VS Code, with clickable links (editors).