Skip to content

feat: ship type declarations generated from the JSDoc - #36

Merged
kurkle merged 1 commit into
chartjs:masterfrom
kurkle:types
Sep 12, 2026
Merged

feat: ship type declarations generated from the JSDoc#36
kurkle merged 1 commit into
chartjs:masterfrom
kurkle:types

Conversation

@kurkle

@kurkle kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member

Closes #16 — open since 2021: Could not find a declaration file for module 'chartjs-test-utils', and every import an implicit any. v1 publishing sources instead of a bundle did not change that; the sources are JavaScript.

@etimberg — the two hand-written pieces below are the parts worth your eye. Everything else is generated.

Generated from the JSDoc, not maintained by hand

npm run types emits the declarations from the JSDoc in src into types/generated, and prepack runs it, so the tarball always carries declarations matching the sources it ships. Nothing is written twice, and nothing can drift.

Chart instances are typed as chart.js's own Chart, through a type-only import. chart.js stays out of dependencies: the constructor is injected into setup({Chart}), and any consumer of this package has chart.js by definition. If you would rather not have the type reference at all, the alternative is any for every chart — a narrower hand-written interface would be worse than both, since it would reject the real Chart API.

Two things a declaration emit cannot produce

types/entry.d.ts is hand-written and re-exports the generated entry, plus:

The matchers. They live on Vitest's expect, not in this package's exports, so the entry augments Vitest's Matchers interface. That is what makes this typecheck in a consumer:

expect(chart).toEqualImageData(expected, {tolerance: 0.002});
expect(chart).toBeChartOfSize({dh: 256, dw: 256, rh: 256, rw: 256});

The mock context's methods. createMockContext() assigns them in a loop, so the emit sees only record, getCalls and resetCallsctx.fillRect(...) did not typecheck. They are now declared as Pick<CanvasRenderingContext2D, ...>, so the signatures come from the DOM library rather than being retyped by me; measureText is spelled out, because the mock returns a fixed subset of TextMetrics.

Both hand-written lists are guarded rather than trusted: src/context.js exports its method table, and two unit specs compare the names in types/entry.d.ts against the implementation. Verified by adding ellipse to the mock and watching them fail. That is the one new coupling this PR introduces, and it fails loudly.

How the published shape was checked

test/types/consumer.ts compiles the package the way a consumer sees it — imports through the package name, so the exports map decides which types are found, and skipLibCheck: false, so the declarations are actually checked instead of skipped. It is not a test of the implementation; it is a test of the contract.

It earned its place while being written, by failing twice: once on the untyped mock context, once on a chart.js Element that does not declare getCenterPoint. Its @ts-expect-error lines are assertions in the same spirit — the build fails if the types stop rejecting setup({}), a partial toBeChartOfSize, or fillRect('0', 0, 1, 1).

On the packed tarball:

root entry ./node
node10 🟢 🟢
node16 from ESM 🟢 🟢
bundler 🟢 🟢
node16 from CJS ⚠️ ESM, dynamic import only ⚠️ ESM, dynamic import only

The ./node subpath needed a typesVersions map, because node10 resolution does not read exports. The CJS warning is inherent to the package being ESM-only and predates this change. publint is clean apart from its standing sideEffects suggestion, which I left alone: src/spriting.js decodes the sprite sheet at import time, so "sideEffects": false would be a lie a bundler is entitled to act on.

npm test covers it end to end: Biome, the tooling typecheck, the consumer typecheck, 9 node specs and 26 browser specs in Chromium and Firefox.

Note on the release

This is a feat: title, so with #35 it would publish 1.1.0. If #35 is not merged first, the version needs bumping by hand as before.

🤖 Generated with Claude Code

Closes chartjs#16, open since 2021: a TypeScript consumer got "Could not find a
declaration file for module 'chartjs-test-utils'" and every import was `any`.
v1 publishes its sources rather than a bundle, which did not change that -- the
sources are JavaScript.

The declarations are emitted from the JSDoc by `npm run types` into
`types/generated`, which `prepack` runs, so the published tarball always has
declarations matching the published sources and nothing has to be maintained
twice. Chart instances are typed as chart.js's own `Chart` through a type-only
import; chart.js stays out of `dependencies`, because the constructor is
injected into `setup()` and every consumer of this package has chart.js by
definition.

Two things a declaration emit cannot produce, so `types/entry.d.ts` declares
them by hand:

- **The matchers.** They live on Vitest's `expect`, not in this package's
  exports, so the entry augments Vitest's `Matchers` interface. That is what
  makes `expect(chart).toEqualImageData(...)` typecheck for a consumer.
- **The mock context's methods.** `createMockContext()` assigns them in a loop,
  so the emit sees only `record`/`getCalls`/`resetCalls`. They are declared as
  `Pick<CanvasRenderingContext2D, ...>`, so the signatures come from the DOM
  library rather than being retyped, with `measureText` spelled out because the
  mock returns a fixed subset of TextMetrics.

Both hand-written lists are guarded: `src/context.js` now exports its method
table, and two unit specs compare the names in `types/entry.d.ts` against the
implementation -- verified by adding a method to the mock and watching them
fail. This is the only new coupling, and it fails loudly rather than silently.

`test/types/consumer.ts` compiles the published shape the way a consumer sees
it: imports through the package name, so the `exports` map picks the types, and
`skipLibCheck: false`, so the declarations themselves are checked rather than
skipped. It caught two real problems while being written -- the untyped mock
context, and a chart.js element that does not declare `getCenterPoint`. Its
`@ts-expect-error` lines are assertions too: they fail the build if the types
stop rejecting `setup({})`, a partial `toBeChartOfSize`, or `fillRect('0', ...)`.

Measured on the packed tarball: `attw` is green for node10, node16 from ESM and
bundler resolution, on both the root entry and `./node` -- the latter needs a
`typesVersions` map, since node10 does not read `exports`. The remaining `attw`
warning is that a `require` call resolves to ESM, which is inherent to the
package being ESM-only and predates this change. `publint` is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kurkle
kurkle requested a review from etimberg September 12, 2026 17:11
@kurkle kurkle mentioned this pull request Sep 12, 2026
@kurkle
kurkle merged commit 9bdf0cb into chartjs:master Sep 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing declaration file

2 participants