Work in progress 👷
tydoc
- Features
- Features Still TODO
- General Features Overview
- Type Index Overview
- TS Types Support Overview
- AST Guide
-
API
renderMarkdown
fromProject
fromModule
- Exported Types
-
Type Index
I
Options
T
DocPackage
&
DocModule
T
TSDocFrag
I
TSDoc
&
DocTypeUnion
|
Node
T
DocTypePrimitive
T
DocTypeLiteral
&
DocTypeAlias
T
RawFrag
&
DocTypeInterface
T
DocProp
&
DocTypeCallable
T
DocSig
T
DocSigParam
T
DocTypeArray
&
DocTypeObject
T
DocTypeIndexRef
&
DocUnsupported
&
DocTypeIntersection
T
Expor
I
Options
I
Settings
F
Thunk
- Debugging
Features
- CLI
- API
- JSON output
- Markdown output
- Support for named exports
- Support for default exports
- Support for packages with multiple entrypoints
- Automatic main module detection
- Type Index to dedupe type referenes
- Support for different same-name types by using module paths to disambiguate (fully qualified type name aka. "FQTN")
- Interfaces
- Type aliases
- Support for unions with multiple discriminants
- Function types
- Overloaded function types
- Callable namespace types (functions with properties)
- Union types
- Literal types
- Discriminant union types
- Intersection types
- Array types
- Inline Object types
- Typeof operator
Features Still TODO
- Parse jsdoc on extracted items
- Parse jsdoc on modules
- classes
- enums
- generics
-
RegExp
Type -
Date
Type - Parameter defaults
- Indexable Types
- Tuple Types
General Features Overview
Note: The following feature examples show JSON in JSON5 format so that comments may be rendered with the examples. Just understand that Tydoc actually emits JSON, not JSON5.
Note: The JSON output of Tydoc is verbose. Examples here show only a minimal subset of the data as required to make the point in any given example. Just understand that there are many more aspects to the data than any one example shows at a time.
CLI
Tydoc comes with a CLI. Use the project
sub-command at the root of your node package (where package.json
resides) to generate your API documentation.
$ cd my/node/package
$ tydoc project a b ...
Pass as many entrypoints as there are in your node package. All paths given are relative to your source root. Source root is the rootDir
as configured in your tsconfig.json
. If not set then source root is the current working directory.
JSON Representation
Tydoc can extract documentation as JSON so that you can render it however you want.
You can generate JSON from the CLI like so:
tydoc project --json main > docs.json
The structure is fully typed. You can leverage the typings like so:
import docPackageJson from './docs.json'
import { Doc } from 'tydoc/types'
const docs = docPackageJson as Doc.DocPackage
The types are extensively documented inline so read them to learn more about the shape/schema of the JSON data and semantics of fields therein (e.g. is a path relative or not).
Markdown Representation
There is a bundled markdown renderer. You can use it from the command line like so:
$ tydoc project --markdown main > docs.md
For an example of what the output looks like see the Tydoc repo README.md
"API" section.
Automatic Main-Entrypoint Detection
Theory
All valid Node packages must specify their main entrtypoint in package.json
like so:
{
"name": "somePackage",
"main": "./path/to/main.js"
}
The main entrypoint can be imported without path qualification, while all other module imports from the package must be done via explicit paths:
import { foo } from 'somePackage' // <somePackagePath>/path/to/main.js
import { bar } from 'somePackage/not/main/one'
import { qux } from 'somePackage/not/main/two'
Tydoc Support
Tydoc takes advantage of this to automatically detect when the entrypoint it is extracting is the main one or not.
For example:
$ tydoc project path/to/main
Leads to extracted JSON:
{
modules: [
{
isMain: true,
},
],
}
Multiple Entrypoint Support
What does "multiple entrypoints" mean
A package with multiple entrypoints means that your package has more than one module where your users are allowed to import things from. For example in the following b.ts
and c.ts
are official entrypoints into the package in addition to a
.
a.ts
b.ts
c.ts
package.json <- "name": "some-package"
Thus allowing users to do e.g.:
import { foo } from 'some-package' // a
import { bar } from 'some-package/b'
import { qux } from 'some-package/c'
Tydoc support
Tydoc accepts multiple entrypoints on the CLI:
tydoc project a b c
Named Exports
Tydoc captures all named exports of a module.
For example:
export const a = 0
export const b = 1
export const c = 2
{
modules: [
{
namedExports: [
{
name: 'a',
},
{
name: 'b',
},
{
name: 'c',
},
],
},
],
}
Main Export
Tydoc captures the main export of a module. The "main" export is the one exported using export default ...
syntax. Note that main exports are nameless by design and so Tydoc will naturally not extract a name either.
For example:
const a = 0
export default a
{
"modules": [
{
"mainExport": {
}
]
}
Type Index Overview
The types in a package are a graph of references, so Tydoc always creates a type index. What this means is that when Tydoc is extracting type information from your package, it keeps extracted types in the type index rather than documenting types inline.
Exported Types of exported terms reference the type index
For example in the following module the Foo
type will be indexed and referenced by the two named exports here. Note that Foo
type is treated as both a named export and a type in the type index. Tydoc decouples the concepts.
export type Foo = { a: string }
export const foo: Foo = { a: 'bar' }
{
modules: [
{
namedExports: [
{
name: 'Foo',
type: {
link: '(test).Foo',
},
},
{
name: 'foo',
type: {
link: '(test).Foo',
},
},
],
},
],
typeIndex: {
'(test).Foo': {
kind: 'alias',
name: 'Foo',
type: {
kind: 'object',
props: [
{
kind: 'prop',
name: 'a',
type: {
kind: 'primitive',
type: 'string',
},
},
],
},
},
},
}
Non-exported types of exported terms show up in the Type Index
For example in the following module Foo
is not exported. But since the exported term foo
references it, it will still be part of the type index.
In this way the Type Index is a representation of all the possible types that your users could encounter as they use your package.
type Foo = { a: string }
export const foo: Foo = { a: 'bar' }
{
modules: [
{
namedExports: [
{
name: 'foo',
type: {
link: '(test).Foo',
},
},
],
},
],
typeIndex: {
'(test).Foo': {
kind: 'alias',
name: 'Foo',
type: {
kind: 'object',
props: [
{
kind: 'prop',
name: 'a',
type: {
kind: 'primitive',
type: 'string',
},
},
],
},
},
},
}
Module Level TsDoc
If you write TsDoc at the top of your module then it will be treated as module-level documentation.
For example:
/**
* Welcome to MyAwesomePackage...
*/
{
modules: [
{
tsdoc: {
raw: '/**\n * Welcome to MyAwesomePackage...\n */',
summary: 'Welcome to MyAwesomePackage...',
examples: [],
customTags: [],
},
},
],
}
Note that if you have non-import syntax below the the TsDoc then the TsDoc will be assumed to be part of that syntax node.
For example:
/**
* Welcome to MyAwesomePackage...
*/
function foo() {}
{
modules: [
{
tsdoc: null,
},
],
}
To handle this case, make sure that the syntax node has its own TsDoc.
For example:
/**
* Welcome to MyAwesomePackage...
*/
/**
* Whatever
*/
function foo() {}
{
modules: [
{
tsdoc: {
raw: '/**\n * Welcome to MyAwesomePackage...\n */',
summary: 'Welcome to MyAwesomePackage...',
examples: [],
customTags: [],
},
},
],
}
Qualified Module Paths
Tydoc fully qualifies each type name in the type index. This version of a type name is referred to as its "fully qualified type name", abbreviated as FQTN. The FQTN makes it possible to keep different types with the same name in the index.
For example:
// a.ts
import * as B from './foo/bar/b'
import * as A from './tim/buk/c'
export const a: B.Foo = { b: 1 }
export const b: C.Foo = { c: 2 }
// b.ts
export type Foo = { b: 2 }
// c.ts
export type Foo = { c: 3 }
Would result in two Foo
types in the type index, qualified with paths b
and c
:
{
typeIndex: {
'(foo/bar/b).Foo': {},
'(tim/buk/c).Foo': {},
},
}
Avoids extracting docs for native types (Array, RegExp, etc.)
TS Types Support Overview
Functions
export function foo(x: string, y: boolean): number {
// ...
}
{
modules: [
{
kind: 'module',
namedExports: [
{
kind: 'export',
name: 'foo',
type: {
kind: 'callable',
isOverloaded: false,
hasProps: false,
props: [],
sigs: [ <-- guaranteed one signature here becuase `isOverloaded` is `false`
{
kind: 'sig',
return: {
kind: 'primitive',
type: 'number',
},
params: [
{
kind: 'sigParam',
name: 'x',
type: {
kind: 'primitive',
type: 'string',
},
},
{
kind: 'sigParam',
name: 'y',
type: {
kind: 'primitive',
type: 'boolean',
},
},
],
},
],
},
},
],
},
],
}
Overloaded Functions
function foo(x: RegExp): boolean
function foo(a: string, b: number): number
function foo(...args: [RegExp] | [string, number]) {
// ...
}
export { foo }
{
modules: [
{
kind: 'module',
namedExports: [
{
kind: 'export',
name: 'foo',
type: {
kind: 'callable',
isOverloaded: true, // <-- true
hasProps: false,
props: [],
sigs: [
// v-- All signatures are contained in this array
{
kind: 'sig',
return: {
kind: 'primitive',
type: 'boolean',
},
params: [
{
kind: 'sigParam',
name: 'x',
type: {
kind: 'unsupported',
},
},
],
},
{
kind: 'sig',
return: {
kind: 'primitive',
type: 'number',
},
params: [
{
kind: 'sigParam',
name: 'a',
type: {
kind: 'primitive',
type: 'string',
},
},
{
kind: 'sigParam',
name: 'b',
type: {
kind: 'primitive',
type: 'number',
},
},
],
},
],
},
},
],
},
],
}
Callable Namespaces
Tydoc supports callable namespaces. These are basically functions with properties. In TS they can be represented like so:
interface foo {
(x: string): boolean
bar: string
qux: number
}
In JS it could look like:
function foo(x) {
//...
}
foo.bar = 'something'
foo.qux = 0
In Tydoc:
{
typeIndex: {
'(a).foo': {
kind: 'callable',
hasProps: true, // <-- true
props: [
// v--- all properties of the namespace are in this array
{
kind: 'prop',
name: 'a',
type: {
kind: 'literal',
base: 'number',
name: '1',
},
},
{
kind: 'prop',
name: 'b',
type: {
kind: 'literal',
base: 'number',
name: '2',
},
},
],
sigs: [
{
kind: 'sig',
params: [],
return: {
kind: 'primitive',
type: 'boolean',
},
},
],
},
},
}
Union Types
export type Foo = 'a' | 'b'
{
typeIndex: {
'(example).Foo': {
kind: 'alias',
name: 'Foo',
type: {
kind: 'union',
isDiscriminated: false,
discriminantProperties: null,
types: [
{
kind: 'literal',
name: '"a"',
base: 'string',
},
{
kind: 'literal',
name: '"b"',
base: 'string',
},
],
},
},
},
}
Discriminant Union Types
Theory
TypeScript supports the concept of discriminant union types. It simply means that among the members of the union there is a common property that can be used at runtime to narrow which member is being worked with.
type Fruit = Apple | Banana
type Apple = { kind: 'Apple'; crispy: boolean }
type Banana = { kind: 'Banana'; slippery: boolean }
const members = [
{ kind: 'Apple', crispy: true },
{ kind: 'Banana', slippery: false }
] as const
const fruit: Fruit = members[Math.floor(Math.random() * 2]
if (fruit.kind === 'Apple') {
fruit.crispy // narrows to type Apple
}
if (fruit.kind === 'Banana') {
fruit.slippery // narrows to type Banana
}
There are two requirements for a property to be discriminant:
- The type of the property must be a literal. Often a string, but it could just as well be
1
2
false
etc. Another requirement - The types of the properties between union members must not overlap. For example if all
kind
properties above were of typehello
then the checks would not be sufficient to know which member is being worked with. TS statically enforces this.
Tydoc Support
Tydoc will detect if all members of a union type have a property in common whose type does not overlap and if so mark the union type as being descriminated and capture which property is the discriminant. If multiple properties could act as discriminants then Tydoc captures them all.
export type A = B | C
type B = { b: 2; kind1: 'B1'; kind2: 'B2' }
type C = { c: 3; kind1: 'C1'; kind2: 'C2' }
{
typeIndex: {
'(a).A': {
kind: 'alias',
name: 'A',
type: {
kind: 'union',
discriminantProperties: ['kind1', 'kind2'],
isDiscriminated: true,
},
},
},
}
Intersection Types
export type A = { s: string } & { b: boolean }
{
typeIndex: {
'(a).A': {
kind: 'alias',
name: 'A',
type: {
kind: 'intersection',
types: [
{
kind: 'object',
props: [
{
kind: 'prop',
name: 's',
type: {
kind: 'primitive',
type: 'string',
},
},
],
},
{
kind: 'object',
props: [
{
kind: 'prop',
name: 'b',
type: {
kind: 'primitive',
type: 'boolean',
},
},
],
},
],
},
},
},
}
Interface Types
export interface foo {
bar: string
qux: number
}
{
typeIndex: {
'(example).foo': {
kind: 'interface',
name: 'foo',
props: [
{
kind: 'prop',
name: 'bar',
type: {
kind: 'primitive',
type: 'string',
},
},
{
kind: 'prop',
name: 'qux',
type: {
kind: 'primitive',
type: 'number',
},
},
],
},
},
}
Array Types
Tydoc avoids extracting docs for array types since they are native. Tydoc simply traverses into the member types as you would expect.
export type foo = string[]
{
typeIndex: {
'(example).foo': {
kind: 'alias',
name: 'foo',
type: {
kind: 'array',
innerType: {
kind: 'primitive',
type: 'string',
},
},
},
},
}
Literal Types
export type Foo = 'bar'
export type Qux = 1
export type Wuf = false
{
typeIndex: {
'(example).Foo': {
kind: 'alias',
name: 'Foo',
type: {
kind: 'literal',
name: '"bar"',
base: 'string',
},
},
'(example).Qux': {
kind: 'alias',
name: 'Qux',
type: {
kind: 'literal',
name: '1',
base: 'number',
},
},
'(example).Wuf': {
kind: 'alias',
name: 'Wuf',
type: {
kind: 'literal',
name: 'false',
base: 'boolean',
},
},
},
}
Inline Object types
export type Foo = {
bar: string
}
{
typeIndex: {
'(example).Foo': {
kind: 'alias',
name: 'Foo',
type: {
kind: 'object', // <--
props: [
{
kind: 'prop',
name: 'bar',
type: {
kind: 'primitive',
type: 'string',
},
},
],
},
},
},
}
typeof
operator
const foo = 1
export type Bar = {
foo: typeof foo
}
{
modules: [
{
namedExports: [
{
name: 'Bar',
type: {
link: '(example).Bar',
},
},
],
},
],
typeIndex: {
'(example).Bar': {
kind: 'alias',
name: 'Bar',
type: {
kind: 'object',
props: [
{
kind: 'prop',
name: 'foo',
// Notice how the type result of calling
// `typeof` has been inlined here.
type: {
kind: 'literal',
name: '1',
base: 'number',
},
},
],
},
},
},
}
EDD Guide
todo
CLI
project
This command extracts documentation data from a TypeScript package. You must tell it where the package entrypoints are. You must also tell it which of the given entrypoints is the main entrypoint. Often a package only has a main entrypoint, and no others. An example of a package with more than one entrypoint is lodash
which has lodash/fp
.
Glossary
A package entrypoint
The main package entrypoint
API
It is possible to use Tydoc in a programmatic way. The CLI is built using this API.
renderMarkdown
(docs: DocPackage, opts: Options) => string
fromProject
(opts: Options) => DocPackage
fromModule
(manager: Manager, sourceFile: SourceFile) => DocPackage
Exported Types
I
RenderMarkdownOptions
typeIndexRef
Type Index
I
Options
export interface Options {
/**
* Whether or not the API terms section should have a title and nest its term
* entries under it. If false, term entry titles are de-nested by one level.
*/
flatTermsSection: boolean
}
T
DocPackage
//
// Package node
//
export type DocPackage = {
modules: DocModule[]
typeIndex: TypeIndex
}
&
DocModule
//
// Module Node
//
export type DocModule = TSDocFrag & {
kind: 'module'
name: string
/**
* The path to this module from package root. If this module is the root
* module then the path will be `/`.
*
* @remarks
*
* This is what a user would place in their import `from `string _following_ the
* package name. For example:
*
* ```ts
* import foo from "@foo/bar/quux/toto"
* // ^^^^^^^^^^
* ```
*/
path: string
isMain: boolean
mainExport: null | Node
namedExports: Expor[]
location: {
absoluteFilePath: string
}
}
T
TSDocFrag
//
// Node Features
//
export type TSDocFrag = {
tsdoc: null | TSDoc
}
I
TSDoc
export interface TSDoc {
raw: string
summary: string
examples: { text: string }[]
customTags: { name: string; text: string }[]
}
&
DocTypeUnion
//
// Union Node
//
export type DocTypeUnion = {
kind: 'union'
isDiscriminated: boolean
discriminantProperties: null | string[]
types: Node[]
} & RawFrag
|
Node
export type Node =
| DocTypeUnion
| DocTypePrimitive
| DocTypeLiteral
| DocTypeAlias
| DocTypeInterface
| DocTypeCallable
| DocTypeArray
| DocTypeObject
| DocTypeIndexRef
| DocUnsupported
| DocTypeIntersection
// todo unused?
| { kind: 'function'; signatures: DocSig[] }
| ({
kind: 'callable_object'
signatures: DocSig[]
properties: DocProp[]
} & RawFrag)
| ({
kind: 'callable_interface'
properties: DocProp[]
signatures: DocSig[]
} & RawFrag)
T
DocTypePrimitive
export type DocTypePrimitive = { kind: 'primitive'; type: string }
T
DocTypeLiteral
export type DocTypeLiteral = { kind: 'literal'; base: string }
&
DocTypeAlias
export type DocTypeAlias = {
kind: 'alias'
name: string
type: Node
} & RawFrag &
TSDocFrag
T
RawFrag
export type RawFrag = {
raw: {
typeText: string
nodeText: string
nodeFullText: string
}
}
&
DocTypeInterface
export type DocTypeInterface = {
kind: 'interface'
name: string
props: DocProp[]
} & RawFrag &
TSDocFrag
T
DocProp
export type DocProp = { kind: 'prop'; name: string; type: Node }
&
DocTypeCallable
export type DocTypeCallable = {
kind: 'callable'
isOverloaded: boolean
hasProps: boolean
sigs: DocSig[]
props: DocProp[]
} & RawFrag
T
DocSig
export type DocSig = { kind: 'sig'; params: DocSigParam[]; return: Node }
T
DocSigParam
export type DocSigParam = { kind: 'sigParam'; name: string; type: Node }
T
DocTypeArray
export type DocTypeArray = { kind: 'array'; innerType: Node }
&
DocTypeObject
export type DocTypeObject = { kind: 'object'; props: DocProp[] } & RawFrag
T
DocTypeIndexRef
/**
* A link to the type index. All named types go into the type index. When a type
* or export includes a named type, rather than documenting it inline, a
* reference to the type index is created.
*
*/
export type DocTypeIndexRef = {
kind: 'typeIndexRef'
/**
* An identifier that can be used to lookup the type in the type index.
*
* @example
*
* ```ts
* docs.typeIndex[typeIndexRef.link]
* ```
*/
link: string
}
&
DocUnsupported
export type DocUnsupported = { kind: 'unsupported' } & RawFrag
&
DocTypeIntersection
//
// Intersection Node
//
export type DocTypeIntersection = {
kind: 'intersection'
types: Node[]
} & RawFrag
T
Expor
//
// Export Node
//
export type Expor = {
kind: 'export'
name: string
isTerm: boolean
isType: boolean
type: Node
}
I
Options
interface Options {
/**
* Paths to modules in project, relative to project root or absolute.
*/
entrypoints: string[]
project?: tsm.Project
/**
* Specify the path to the package's entrypoint file.
*
* @defualt Read from package.json main field
* @remarks This is useful for tests to avoid mocks or environment setup
*/
packageMainEntrypoint?: string
/**
* Specify the root of the project.
*
* @default The current working directory
* @remarks This is useful for tests to avoid having to mock process.cwd
*/
prjDir?: string
readSettingsFromJSON: boolean
/**
* Sometimes a source entrypoint is fronted by a facade module that allows
* package consumers to do e.g. `import foo from "bar/toto"` _instead of_
* `import foo from "bar/dist/toto". Use this mapping to force tydoc to view
* the given source modules (keys) at the given package path (values).
*
* @example
*
* Given project layout:
*
* ```
* /src/foo/bar/toto.ts
* ```
*
* The setting:
*
* ```ts
* sourceModuleToPackagePathMappings: {
* "foo/bar/toto": "toto"
* }
* ```
*
* Will cause the `toto` module to be documented as being available at path:
*
* ```ts
* import some from "thing/toto"
* ```
*/
sourceModuleToPackagePathMappings?: Record<string, string>
}
I
Settings
export interface Settings {
/**
* Absolute path to the source root. This should match the path that rootDir
* resolves to from the project's tsconfig.json.
*/
srcDir: string
prjDir: string
mainModuleFilePathAbs: string
sourceModuleToPackagePathMappings?: Record<string, string>
}
F
Thunk
export type Thunk<T> = () => T
Debugging
Tydoc uses debug. When enabled via *
it also causes Oclif to render stack traces for unexpected thrown errors.
DEBUG=* tydoc ...