---
title: "TypeScript"
description: "The exported surface comprises named prop interfaces, the ClinicalStatus and HealthCategory unions, and the rule that makes generated API tables possible."
url: "https://opsinjs.pensievelabs.org/handbook/typescript"
source: "https://opsinjs.pensievelabs.org/handbook/typescript.md"
section: "Handbook"
kind: "handbook"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["types", "ClinicalStatus type", "HealthCategory type", "props interface", "generated props table"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="handbook" />

## The short version [#the-short-version]

<NotBuiltYet />

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](../health/two-colour-axes.mdx), 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 [#how-it-works]

### The semantic unions [#the-semantic-unions]

```ts
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 [#named-props-interfaces]

```ts
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](../reference/generated/types.mdx), 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 [#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 [#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 [#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 [#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.

## Related [#related]

* [Reference → Types](../reference/generated/types.mdx) is the generated list of
  every exported symbol.
* [Naming conventions](./naming-conventions.mdx) says what to call the
  interface, the props and the files.
* [Code style](./contributing/code-style.mdx) has the named-props-interface rule
  as a contribution requirement.
* [Clinical status semantics](../health/clinical-status-semantics.mdx) says what
  each `ClinicalStatus` member is allowed to mean.
