9.2 KiB
JSDoc Patterns
@category Guidance
When adding or vetting JSDoc categories in public source files:
- Use exactly one
@categorytag for each public JSDoc block that represents a documented API. - Use shared categories consistently across the repository. Domain-specific categories are allowed when they improve navigation within a file or package, but avoid one-off categories unless they name an important API/domain concept.
- Prefer lowercase category names by default, plural nouns for API buckets, and gerunds for operation families.
- Preserve canonical casing for acronyms and proper API/domain names, such as
type IDs,DateTime,Undici, andHttpAgent. - Prefer shared API-shape categories for common Effect/library patterns, and use domain-topic categories only when they provide clearer navigation.
- Avoid vague fallback categories. Do not use
utils,common, ormisc; pick a specific shared or domain category instead.
Common Shared Categories
- API shapes:
constructors,destructors,models,schemas,guards,predicates,getters,accessors,instances,constants,protocols,prototypes,re-exports,unsafe,testing - Effect/service concepts:
services,tags,layers,context,resource management,running - Type-level APIs:
utility typesfor type-level helpers/contracts; usemodelsfor exported type/interface/class shapes that represent domain data - Error APIs:
errorsfor error models/classes/types,error handlingfor recovery/catching/mapping APIs - Operations:
combinators,filtering,mapping,sequencing,zipping,combining,merging,converting,transforming,folding,splitting,repetition - Encoding/data formats:
encoding,decoding,serialization - Observability:
tracing,metrics,logging - Other common concepts:
annotations,references,symbols,type IDs,configuration,math,comparisons,ordering
Category Normalization
Normalize category names before adding or reviewing JSDoc:
- Lowercase plain category names. Preserve established acronyms and proper
names, such as
type IDs,DateTime,JSON getters,Base64 getters, andStandard Schema. - Prefer shared plural buckets when the meaning is the same, such as
constructors,models,schemas,guards,getters,services,layers,generators,subscriptions,cookies, andsizes. - Prefer shared operation families over narrow synonyms when precision is not
important, such as
combining,mapping,filtering,folding,converting,transforming,sequencing, andrepetition. - Replace vague fallback categories such as
utils,common,misc, orhelperswith a specific shared or domain category. - Use
servicesforContext.ServiceandContext.Referenceexports, and usetagsonly forContext.Tagexports. - Fix obvious typos and compact variants during cleanup, such as
transferables,re-exports,resource management, andStandard Schema.
Distinctions
Keep these distinctions:
servicesareContext.Service/Context.Referenceexports and service contracts/shapes,tagsareContext.Tagexports, andlayersprovide services.gettersretrieve values/properties, whileaccessorsare contextual service or environment access helpers.errorsare error data types, whileerror handlingis for APIs that handle failures.modelsdescribe domain/API data structures, whileschemasare schema values/combinators andutility typesare type-level helpers/contracts.guardsare TypeScript type guards,predicatesare boolean tests, andfilteringis for filtering operations.
Example Best Practices
Quality Checklist
Use this checklist when authoring or reviewing an example:
-
Classify execution: Make the example clearly one of a runnable observation, typechecked definition, test registration, runtime entrypoint, or external-infrastructure illustration. Do not combine alternative runtimes or deployment paths in one executable module; present them as separately labeled, non-evaluated alternatives.
-
Order setup, operation, observation: Make the documented API and its result scannable. Inline simple setup into the assertion; otherwise arrange setup first, the operation second, and a separate observation block after one blank line.
-
Teach one primary semantic contract: Include only the adjacent concepts needed to observe that behavior. Remove unused errors, services, imports, alternate programs, and fictional generic-type scaffolding. Integration examples may include more concepts only when the integration is the lesson.
-
Observe the promised semantic boundary: Assert the full semantic value when practical. If unstable or irrelevant data requires a projection, choose stable fields that distinguish the promised behavior from neighboring outcomes. For example, prefer
Exit.fail("missing")over observing only_tag === "Failure". -
Prefer direct observation: Use the abstraction's return value, collector, or fold before introducing a mutable probe. Console output or successful execution alone does not establish semantic behavior.
Good:
await Effect.runPromise(Stream.runCollect(Stream.make(1, 2, 3))) // => [1, 2, 3]Counterexample:
const values: Array<number> = [] await Effect.runPromise( Stream.make(1, 2, 3).pipe(Stream.runForEach((value) => Effect.sync(() => values.push(value)))) ) values // => [1, 2, 3] -
Use local probes only when the API has no direct result: A local mutable probe is appropriate for
acquireRelease/finalizers and callback-oriented APIs when lifecycle order or emitted events are the contract. Keep the probe local and sequential.const events: Array<string> = [] const resource = Effect.acquireRelease( Effect.sync(() => events.push("acquire")), () => Effect.sync(() => events.push("release")) ) await Effect.runPromise(Effect.scoped(resource)) events // => ["acquire", "release"] -
Use Effect-managed observers for concurrency: Prefer
Ref,Deferred, orQueueover mutable arrays or flags when fibers, concurrent consumers, interruption, or races are part of the behavior. -
Keep execution bounded and deterministic: Bound retries, repeats, polling, generated streams, tool loops, and concurrent consumers unless non-termination is the documented entrypoint behavior. Avoid live clocks, randomness, scheduling accidents, external services, machine-specific state, and unawaited work; use controlled inputs and ensure cleanup completes.
-
Choose runners deliberately: Prefer awaited
Effect.runPromisein runnable examples. UseEffect.runSynconly when synchronous execution is the documented contract or materially clarifies an Effect known to be synchronous; do not use it merely as a shorter doctest runner. -
Progress multiple examples by behavior: Move from basic success to a defining boundary or failure, then composition or lifecycle behavior. Do not repeat equivalent happy paths with renamed values or alternate syntax unless the calling style or overload dispatch is itself part of the contract.
-
Keep type-only examples type-only: Retain runnable metadata for extraction and typechecking, but do not add tautological runtime assertions such as assigning a typed literal and asserting that the literal is unchanged. Add an assertion only when the API also performs runtime transformation or validation.
Doctest Mechanics
- Mark runnable TypeScript examples with `````ts import.meta.vitest`` so
pnpm doctestexecutes them. - Use a trailing
// =>comment to assert an expression or single initializedconstidentifier against a TypeScript expression on the same line. Values use Effect'sEqual.equalssemantics, and examples without markers remain execution-only. Write asynchronous execution explicitly; the transform does not run Effects or await promises automatically. - Prefer asserting the API call directly. Keep bindings only for reuse, mutation, identity checks, or meaningful multi-step setup; put a blank line before a separate assertion block.
- Keep calls on one line when the complete line is at most 120 characters. Format expected arrays densely (
[1, 2],[[1], [2]],Option.some([1, 2])) while retaining normal object spacing. - Assert semantic constructors such as
Option.some,Result.succeed, andExit.fail, not rendered console output. Preserve runnable markers on type-level examples without adding fake runtime assertions. - Keep runnable examples complete, deterministic, bounded, and independent of external services or machine-specific state. Await asynchronous work so failures and cleanup remain inside the doctest.
- Import public APIs and include all required setup. Do not use undeclared placeholders or rely on declarations from surrounding prose.
- Leave examples that register Vitest tests or suites as plain `````ts`` fences; the doctest collector executes runnable snippets inside tests, where nested test registration is invalid. Invoke registration APIs directly so the snippet still shows the intended top-level usage.
- Leave intentionally non-executable snippets as plain `````ts`` fences.
- Run
pnpm doctest --run <source files>from the repository root after changing runnable examples.