opsinjs
HandbookEnvironment

TypeScript

The exported surface comprises named prop interfaces, the ClinicalStatus and HealthCategory unions, and the rule that makes generated API tables possible.

The short version

NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.

This component is not built yet

There is nothing to render because there is nothing to install. What you can read on this page is the specification the implementation will have to satisfy.

PlannedRoadmapWhat “planned” means

Two rules define the TypeScript surface, and the second one is load-bearing for the documentation site itself:

  1. The semantic vocabularies are exported unions, not strings. ClinicalStatus and HealthCategory are the type-level form of the two colour axes, and using them means a mixed axis is a compile error rather than a review comment.
  2. Every component exports a named props interface. Not an inline object type, not a bare ComponentProps<typeof Base.Root>. This is what fumadocs-typescript reads to generate the API table on each component page, so a component without a named interface has no API documentation at all.

How it works

The semantic unions

export type ClinicalStatus = "steady" | "watch" | "attention" | "urgent"
export type HealthCategory =
  | "sleep" | "heart" | "activity" | "nutrition" | "mind" | "labs"

These are ordinal in the first case and unordered in the second, and they are deliberately not merged into one "variant" prop. A component that accepts both accepts them as two props, which is how the never-mix rule survives contact with autocomplete.

Named props interfaces

export interface ResultCardProps extends React.ComponentPropsWithRef<"div"> {
  /** The clinical status of the value being shown. */
  status?: ClinicalStatus
  /** The metric family this result belongs to. */
  category?: HealthCategory
  /** @internal */
  debugSlot?: React.ReactNode
}

What the generator does with it: reads the interface by name, renders each member with its JSDoc as the description, surfaces defaults, marks required members, links every referenced type name into Reference → Types, and suppresses anything tagged @internal. Props inherited unchanged from Base UI are delegated with one sentence and a link rather than re-documented. opsinjs documents only what it adds.

The consequence is that JSDoc is documentation, not a comment. A prop with no doc comment appears in the table with an empty description, permanently, in public.

Configuration

strict: true is on. allowImportingTsExtensions: true is set because the .mts build scripts import lib/color/*.ts with the explicit extension Node requires; noEmit: true makes that legal. noUncheckedIndexedAccess is deliberately off. See the note in tsconfig.base.json.

Do this

  • Name the interface <Component>Props and export it. The generator looks it up by that name.
  • Write a JSDoc line for every public prop, in the same voice as the rest of the documentation: what it does, not what it is.
  • Use @internal for anything you do not want documented, rather than omitting the comment and hoping.
  • Extend React.ComponentPropsWithRef<"element"> so consumers get the native props and the ref type without your enumerating them.
  • Type your own wrappers from the exported interface, not by copying the props. Pick, Omit and intersection keep you honest across upgrades.
  • Import types with import type so they are erased and cannot accidentally create a runtime dependency.

Not this

  • Do not use an inline object type for props. The generator cannot resolve it, so the component ships with an empty API table and nobody notices until a reader asks.
  • Do not re-export ComponentProps<typeof Base.Root> as your props type. It produces a table of Base UI internals and hides the props you added.
  • Do not widen status to string. It removes the only compile-time protection the two-axis rule has.
  • Do not use an enum. The build scripts are erasable-syntax-only TypeScript run by plain node; enums are not erasable and will fail at runtime with an error that does not mention enums.
  • Do not export a type only from a .d.ts. The generator reads the source.

Gotchas

  • A renamed interface silently empties the API table. No build error. The generator finds nothing and renders nothing. assert-ia is the backstop.
  • ComponentProps and ComponentPropsWithRef differ in React 19 in ways that matter for ref typing in a render prop; picking the wrong one produces an error at the call site rather than at the definition.
  • allowImportingTsExtensions needs noEmit. Without it, tsc fails with TS5097 pointing at the import rather than at the configuration.
  • .mts files run by node must be erasable-syntax-only. No enums, no parameter properties, no namespaces. The failure is a runtime syntax error at the first use.
  • Generic components lose their generic through a naive forwardRef wrapper. The type becomes unknown and every call site widens.
  • A type-only circular import type-checks but breaks the generator, which resolves modules eagerly.

On this page