TypeScript Documentation Best Practices for Growing Teams
Strong TypeScript types catch bugs early, but without documentation, the meaning behind them gets lost. Here's how growing teams keep their types self-explanatory.
DocPilot Team
AI documentation research and engineering from Onzira Labs.
TypeScript Documentation Best Practices for Growing Teams
Your team added TypeScript six months ago. The compiler caught hundreds of bugs before they hit production, and adoption spread fast. Now you have 40,000 lines of .ts files, junior developers onboarding every quarter, and a recurring Slack thread: "what does this Pick<T, K> actually return?"
The problem isn't TypeScript itself. It's the gap between types that exist and types that explain themselves. Strong types answer "what shape is this data?" but rarely answer "why does it exist this way?"
This guide covers the documentation practices that scale as TypeScript codebases grow. No fluff, no theoretical purity — just the rules that hold up when five engineers become fifty.
Why Types Alone Aren't Documentation
A well-typed function signature tells you what shape it accepts and returns. It does not tell you:
- Whether the function is safe to call concurrently
- Which exceptions or rejected promises it produces
- Which arguments are mutually exclusive
- What the function does in edge cases (empty arrays, null, undefined)
- Why a particular design decision was made
Consider this signature:
function processOrder(order: Order, options: Options): Promise<Result>;
It's type-safe. It's also nearly useless without context. What does Options configure? What happens when order.status === 'cancelled'? Does this hit the network?
A growing team cannot afford to rediscover this knowledge every time a new engineer touches the file.
Use TSDoc for Public APIs
TSDoc is the de facto standard for documenting TypeScript APIs. It's a structured comment format that integrates with editors, build tools, and documentation generators like TypeDoc, API Extractor, and DocPilot.
The Minimum Viable TSDoc Block
Every exported declaration should have at least:
/**
* Charges a customer's payment method for an order.
*
* @param order - The validated order to charge. Must have a non-zero total.
* @param options - Charge configuration. See {@link ChargeOptions}.
* @returns A {@link ChargeResult} describing the outcome.
*
* @throws {ValidationError} If the order has line items with zero quantity.
* @throws {PaymentDeclinedError} If the issuer rejects the charge.
*
* @example
* ```ts
* const result = await chargeOrder(order, { idempotencyKey: 'abc' });
* if (result.status === 'success') {
* await fulfillOrder(order.id);
* }
* ```
*/
export async function chargeOrder(
order: Order,
options: ChargeOptions
): Promise<ChargeResult>;
The @param, @returns, @throws, and @example tags form a contract that survives team turnover. Crucially, the example shows idiomatic usage, not just syntactically valid usage.
Tags That Actually Matter
Not all TSDoc tags earn their keep. Here are the ones with the highest return on effort:
| Tag | Use case | Why it matters |
|---|---|---|
@param | Function arguments | Forces you to think about null/empty cases |
@returns | Return values | Clarifies success vs. partial success states |
@throws | Thrown errors | Critical for promise-based and error-handling code |
@example | Usage patterns | The single most-read part of any doc block |
@remarks | Extended context | Use for "why" decisions and design rationale |
@deprecated | Retiring APIs | Editors strike it through; tools warn at use site |
@see | Cross-references | Connects related functions and types |
Avoid @author, @since, and similar attribution tags. They belong in git, not in source.
Document Types, Not Just Functions
TypeScript teams often forget to document the types they create. A complex union or discriminated union deserves explanation.
/**
* Result of an authentication attempt.
*
* Use the `kind` field to narrow before accessing variant-specific properties.
* The `error` field is only meaningful when `kind === 'failure'`.
*
* @remarks
* We intentionally do not throw on auth failure. Throwing makes it impossible
* to handle failed sign-ins without try/catch, which obscures control flow
* in UI code. Return a result type instead.
*/
export type AuthResult =
| { kind: 'success'; user: User; session: Session }
| { kind: 'failure'; error: AuthError; attemptId: string }
| { kind: 'rate-limited'; retryAfterMs: number };
Notice how the @remarks section captures a design decision. Six months later, when a new engineer suggests "why don't we just throw?", the answer is already in the source. This is the single highest-leverage documentation practice for growing teams: capture decisions, not just descriptions.
Use JSDoc Inside JavaScript Files
Even in TypeScript projects, .js files still exist — config files, migration scripts, build tooling. JSDoc with TypeScript-checking enabled gives you type safety without a build step:
/**
* @typedef {Object} MigrationRecord
* @property {string} id
* @property {number} version
* @property {Record<string, unknown>} payload
*/
/**
* Migrates a record to the latest schema version.
* @param {MigrationRecord} record
* @returns {Promise<MigrationRecord>}
*/
export async function migrate(record) {
// ...
}
Configure allowJs and checkJs in tsconfig.json to get IntelliSense and type errors. This is genuinely useful for one-off scripts that don't justify a full tsconfig.
Automate Enforcement or It Won't Happen
Documentation rots faster than code. The only reliable defense is automation. Set these up in CI:
1. ESLint with eslint-plugin-tsdoc
{
"rules": {
"tsdoc/syntax": "error",
"require-jsdoc": ["warn", {
"publicOnly": true,
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true,
"ArrowFunctionExpression": false,
"FunctionExpression": false
}
}]
}
}
Block PRs that remove documentation without justification.
2. TypeDoc or DocPilot for Generated Docs
Hand-maintained docs drift. Generated docs stay accurate if you commit to reading them. Pick a generator, build it on every release, and host it where the team can see it (usually a /docs route on your internal site).
3. Public API Surface Reports
Tools like Microsoft's API Extractor and API Report track changes to your library's exported surface. When a new export is added without documentation, the report flags it.
Maintain a Style Guide
Every team reaches the same debates: do we use @throws or return result types? Do we document private functions? What's the policy on @example length?
Write down your answers in a STYLEGUIDE.md at the repo root. Keep it under 200 lines. Reference it in PR reviews.
A minimal style guide covers:
- Required TSDoc tags for exported declarations
- When to use
@remarksvs. a separate ADR (Architecture Decision Record) - How to write examples (working code, not pseudo-code)
- What to do for deprecated APIs (timeline, replacement, migration steps)
- Voice and tense (imperative present, "charges the order" not "this will charge")
The style guide does not need to be exhaustive. It needs to exist. Gaps get filled in by PR review, then folded back into the guide.
Common Pitfalls
Over-documenting trivial code
/** Adds one to x. */
const increment = (x: number) => x + 1;
If a function is shorter than its documentation, delete the documentation. The bar for documentation is "would a competent engineer who joined last week understand it without asking?"
Documenting what instead of why
/**
* Loops through users and sends emails.
* @param users - The users
* @returns A promise that resolves when done
*/
export async function emailUsers(users: User[]): Promise<void> { }
This is filler. Say why you batch emails, what happens on partial failure, and which email provider is used.
Stale examples
An @example that doesn't compile is worse than no example. Generate examples from tests, or write them as test cases. TypeDoc 0.23+ supports importing examples from .md files, which keeps them version-controlled and reviewable.
The Payoff
Teams that commit to documentation practices report measurably faster onboarding and fewer clarification questions in code review. The investment is roughly 30 seconds per exported declaration during the original implementation — a cost that compounds into hours saved per new hire, per quarter, per team.
The pattern is consistent: type your code, document the why, automate the what, and write down the rules once.
If your team is drowning in undocumented TypeScript and you need a starting point, DocPilot connects to your GitHub repos and generates TSDoc-style documentation from your existing code, then keeps it in sync as you commit. It's a practical way to bootstrap a documentation habit without a six-week internal tool project. The free tier covers public repos and small teams, so you can see what it produces before you commit to anything.
Read next
api-documentation
The Underrated Power of Well-Structured API Docs
Most API docs fail not because of missing information, but because of missing structure. Here's why architecture beats aesthetics every time.
Read
roadmap
Building a Public Documentation Roadmap: Best Practices
A public documentation roadmap signals that you take your docs seriously. Here's how to build one that earns user trust instead of empty promises.
Read
typescript
How We Built a Multi-Tenant SaaS in TypeScript from Scratch
A pragmatic, code-first walkthrough of designing a multi-tenant SaaS architecture in TypeScript, covering tenant isolation, auth, billing, and the patterns we wish we'd known earlier.
Read