IntermediateTypes, documentation, and package design~40 min

Types, Documentation, and Package Design

JSDoc as editor-visible contracts, TypeScript as an optional neighboring path, and designing a package's public surface — exports, versioning, and documentation.

By the end of this lesson, you can

  • Write a JSDoc comment that gives editor tooling accurate parameter and return types
  • Explain what TypeScript adds over JSDoc, and what gradual adoption looks like
  • Design a package's public exports and version its changes according to semantic versioning
  • Explain why a type is not a substitute for runtime validation at a real boundary

Why it matters

A function’s real contract — what it expects, what it returns, what can go wrong — is invisible to a reader (and an editor) unless it’s written down somewhere machine-readable. JSDoc and TypeScript both make that contract visible; designing a package’s public surface deliberately, with real versioning discipline, is what makes it safe for other code to depend on.

JSDoc: types without a build step

A JSDoc comment gives editor tooling (autocomplete, inline parameter hints, type-mismatch warnings) accurate information, using plain JavaScript with no compilation step required:

JavaScriptA JSDoc comment describing a function's real contract
/**
 * Calculate the total price for a cart, including tax.
 * @param {Array<{ price: number, quantity: number }>} items
 * @param {number} taxRate - A decimal, e.g. 0.08 for 8%.
 * @returns {number} The total price, tax included.
 */
function calculateTotal(items, taxRate) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return subtotal * (1 + taxRate);
}

An editor configured to check JSDoc (most modern ones are, by default, for .js files) will flag calculateTotal('not an array', 0.08) as a type mismatch — real, useful feedback with zero build step.

TypeScript as a neighboring path

TypeScript adds a dedicated type system and compiler on top of JavaScript — the same contracts JSDoc can express, checked more thoroughly, plus features JSDoc can’t express as cleanly (generics, more precise narrowing):

function calculateTotal(items: { price: number; quantity: number }[], taxRate: number): number {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return subtotal * (1 + taxRate);
}

Type narrowing is TypeScript refining a broader type to a more specific one based on a runtime check already in the code:

function describe(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // narrowed to string here — .toUpperCase() is valid
  }
  return value.toFixed(2); // narrowed to number here — .toFixed() is valid
}

Designing a package’s public surface

What a package exports is its contract with everyone who depends on it — changing it is a breaking change for them, whether or not that was the intent:

{
  "name": "my-lib",
  "version": "2.3.1",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}

The exports field explicitly declares what’s public — anything not listed isn’t importable by consumers at all, which is what makes internal refactoring of unexported files safe without breaking anyone.

Semantic versioning: what each version segment promises
SegmentExample bumpMeans
Major2.3.1 → 3.0.0A breaking change — something a consumer relies on now behaves differently or is gone
Minor2.3.1 → 2.4.0A backward-compatible new feature — nothing existing breaks
Patch2.3.1 → 2.3.2A backward-compatible bug fix — behavior gets closer to what was documented, nothing new

A changelog documenting what actually changed per version — ideally generated or at least reviewed alongside every release — is what lets a consumer decide whether upgrading is safe without reading the full diff themselves.

Check your understanding

function process(user) { return user.email.toLowerCase(); } // A JSDoc comment above says: @param {{ email: string }} user // Does adding that JSDoc comment prevent process(null) from throwing at runtime?
Or reveal the answer without checking

Answer:No — JSDoc is editor/tooling information only; it adds no runtime check at all
JSDoc (and TypeScript) provide compile-time/editor-time information, not runtime enforcement. process(null) still throws a TypeError reading .email of null regardless of any type annotation — a real runtime check is a separate, necessary thing.

A library renames an exported function from formatDate to formatDateTime, with no other behavior change. What version bump does semantic versioning call for?
Or reveal the answer without checking

Answer:Major (2.3.1 → 3.0.0) — any consumer importing the old name now breaks
Any consumer that imports formatDate now has a broken import — the public surface changed in a way that breaks existing code, which is exactly what a major version bump signals, regardless of how small the change feels internally.

Practice: warm-up

Write a JSDoc comment for this function describing its parameter and return types accurately.

function findUserById(users, id) {
  return users.find((user) => user.id === id);
}

Stuck? Reveal one hint at a time.

  1. Hint 1

    users is an array of some user shape — { id, name } is a reasonable minimal guess if no fuller shape is given.

  2. Hint 2

    find can return undefined if nothing matches — the return type should reflect that possibility, not just the "found" case.

Reveal a reference solution

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

/**
 * Find a user by id.
 * @param {Array<{ id: number, name: string }>} users
 * @param {number} id
 * @returns {{ id: number, name: string } | undefined}
 */
function findUserById(users, id) {
  return users.find((user) => user.id === id);
}

Practice: apply it

A package’s package.json currently has "version": "1.4.2". For each change, name the correct next version: (1) fixing a bug where a function returned the wrong sign for negative numbers, (2) adding a new optional parameter with a default that preserves old behavior, (3) removing a previously-exported function nobody uses internally anymore.

Stuck? Reveal one hint at a time.

  1. Hint 1

    A bug fix that doesn’t add anything new, and doesn’t change any documented, correct behavior, is the smallest kind of bump.

  2. Hint 2

    An optional addition that doesn’t change existing behavior for anyone not using it yet is the middle kind of bump.

  3. Hint 3

    Removing something a consumer could have been relying on — even if the maintainers think nobody uses it — is the largest kind of bump, since maintainers can’t be certain about every consumer.

Reveal the answers

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

1. Bug fix (wrong sign)              -> 1.4.3 (patch)
2. New optional parameter, safe default -> 1.5.0 (minor)
3. Removing an exported function     -> 2.0.0 (major) — a consumer might still import it,
   regardless of what the maintainers believe usage looks like.

Summary

  • A JSDoc comment gives editor tooling a function’s real contract — parameter and return types — with no build step required; TypeScript adds a dedicated, more thorough type system on top of the same idea, adoptable gradually rather than all at once.
  • Type narrowing refines a broader type to a more specific one based on a runtime check already present in the code.
  • A type is a compile-time/editor-time guarantee only — it enforces nothing once the code actually runs, so data crossing a real boundary still needs runtime validation.
  • A package’s exports field is its actual public contract; anything not listed is safe to refactor internally without breaking consumers.
  • Semantic versioning is a promise about observable behavior, not an internal-effort measurement — a small-feeling change can still be a breaking (major) change if it changes what existing consumers depend on.