Type in, table out. 📐
Your types are already documented — in the JSDoc above each field. type-to-table reads a
TypeScript interface or object type, resolves it with the real TypeScript checker, and writes
a markdown table straight into your README. Re-run it after every change to keep the table in sync
with the type. Works on any plain type — options bags, config objects, API response shapes — no
React, no rendering component, no runtime values required.
React component props are a common special case (props need a mounted-looking component to
resolve against, and there's no defaultProps to read a default from). That case gets its own
entry point: type-to-table/react, powered by react-docgen-typescript. It's an optional peer
dependency — install it only if you use that subpath, and it's never pulled into the base
type-to-table import.
| type-to-table | hand-maintained table | Storybook autodocs | |
|---|---|---|---|
| Source of truth | the type + its JSDoc | whatever you remember to update | the type, via a running Storybook |
| Drift | none — re-run after any type change | guaranteed, eventually | none, but only inside Storybook |
| Output | one markdown table, in your README | markdown, wherever you put it | a browsable UI, not your README |
| Setup | an exported type + JSDoc @default tags |
none | Storybook + a story per component |
| Scope | any interface/type, or React props via a subpath | anything you write by hand | React components only |
Use Storybook when you want an interactive playground. Reach for this when you just want the README table that's already true.
| Version | Highlights |
|---|---|
2.1.0 |
TypeScript types no longer require React |
2.0.0 |
typeGet/typeWrite for plain interfaces and types — no React needed. React props tables moved to the type-to-table/react subpath; react-docgen-typescript is now an optional peer dependency. |
1.0.1 |
Internal changes only |
Full history in CHANGELOG.md.
bun add -D type-to-table # npm / pnpm / yarn all fineUsing the React subpath (type-to-table/react)? Also install its peer dependency:
bun add -D react-docgen-typescriptWrite JSDoc above each field, @default included:
// src/config.ts
export interface ServerOptions {
/** Port to listen on. @default 3000 */
port?: number;
/** Enables verbose request logging. @default false */
verbose?: boolean;
/** Path to the TLS certificate. No default — required in production. */
certPath: string;
}Drop a pair of marker comments into your README — type-to-table replaces whatever sits between
them, so name the tag whatever you like. Then run:
bun run docs:type -- src/config.ts ServerOptions OPTIONS-TABLEThe content between <!-- OPTIONS-TABLE:START --> / <!-- OPTIONS-TABLE:END --> gets replaced
with the generated table.
1 · @default is the only source of the Default column. A type has no runtime value, so
without a @default X tag in the JSDoc, the Default column is just empty for that field.
2 · One pass, safe by construction. | gets escaped before a row is built, so a description
containing a pipe (or a union type like string | number) can't break the table. Long descriptions
can be capped with maxDescriptionLength.
3 · React props need type-to-table/react instead. react-docgen-typescript resolves props
from a real component using the type — FC<Props>, a class component, forwardRef, etc. — not
from the type declaration alone. A type with nothing rendering it has nothing to anchor resolving
on, and tttGet throws No component found. See React props tables below.
import { typeGet, typeWrite } from "type-to-table";Parses filePath, finds the interface or object type alias named typeName, and returns its
fields as a markdown table (Field | Type | Default | Description).
| Option | Type | Default | What it does |
|---|---|---|---|
maxDescriptionLength |
number |
— | Truncates each field's description to this many characters, with …. |
Calls typeGet(filePath, typeName, options) and writes the result into readmePath between
<!-- {tag}:START --> / <!-- {tag}:END --> (via taglify's taglWrite). Returns whether the
file changed.
bun run docs:type -- path/to/file.ts TypeName TAG-NAMEWrites into ./README.md. Wraps typeWrite — see scripts/docs-type.ts
if you need a different target file.
import { tttGet, tttWrite } from "type-to-table/react";Same idea, specialized for React component props via react-docgen-typescript — needs an exported
component (FC<Props>, class, forwardRef, …) using the props type to anchor resolving on; see
The rules above.
// examples/Button.tsx
import type { FC } from "react";
export type ButtonProps = {
/** Button label. */
label: string;
/** Visual style. @default 'primary' */
variant?: "primary" | "secondary";
/** Disables interaction. @default false */
disabled?: boolean;
};
export const Button: FC<ButtonProps> = ({ label }) => label;bun run docs:props -- examples/Button.tsxWrites into ./README.md, tag PROPS-TABLE — see Props table below, generated
from this exact file.
| Option | Type | Default | What it does |
|---|---|---|---|
componentName |
string |
— | Which component to document when the file exports more than one. |
maxDescriptionLength |
number |
— | Truncates each prop's description to this many characters, with …. |
parserOptions |
ParserOptions |
— | Passed straight through to react-docgen-typescript's parse() — e.g. componentNameResolver, customComponentTypes. |
Calls tttGet(filePath, options) and writes the result into readmePath between the tag's marker
comments. Returns whether the file changed.
bun run docs:props -- path/to/Component.tsx [componentName]Generated by running bun run docs:props -- examples/Button.tsx against
examples/Button.tsx — this section is the React subpath's own test case.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Button label. | |
| variant? | "primary" | "secondary" | 'primary' | Visual style. |
| disabled? | boolean | false | Disables interaction. |
bun install
bun run test # bun test
bun run typecheck
bun run build # vite → dist/
bun run format # biome check --write