feat: ship type declarations generated from the JSDoc - #36
Merged
Conversation
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>
Closed
etimberg
approved these changes
Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #16 — open since 2021:
Could not find a declaration file for module 'chartjs-test-utils', and every import an implicitany. 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 typesemits the declarations from the JSDoc insrcintotypes/generated, andprepackruns 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 ofdependencies: the constructor is injected intosetup({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 isanyfor 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.tsis 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'sMatchersinterface. That is what makes this typecheck in a consumer:The mock context's methods.
createMockContext()assigns them in a loop, so the emit sees onlyrecord,getCallsandresetCalls—ctx.fillRect(...)did not typecheck. They are now declared asPick<CanvasRenderingContext2D, ...>, so the signatures come from the DOM library rather than being retyped by me;measureTextis spelled out, because the mock returns a fixed subset ofTextMetrics.Both hand-written lists are guarded rather than trusted:
src/context.jsexports its method table, and two unit specs compare the names intypes/entry.d.tsagainst the implementation. Verified by addingellipseto 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.tscompiles the package the way a consumer sees it — imports through the package name, so theexportsmap decides which types are found, andskipLibCheck: 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
Elementthat does not declaregetCenterPoint. Its@ts-expect-errorlines are assertions in the same spirit — the build fails if the types stop rejectingsetup({}), a partialtoBeChartOfSize, orfillRect('0', 0, 1, 1).On the packed tarball:
./nodeThe
./nodesubpath needed atypesVersionsmap, because node10 resolution does not readexports. The CJS warning is inherent to the package being ESM-only and predates this change.publintis clean apart from its standingsideEffectssuggestion, which I left alone:src/spriting.jsdecodes the sprite sheet at import time, so"sideEffects": falsewould be a lie a bundler is entitled to act on.npm testcovers 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 publish1.1.0. If #35 is not merged first, the version needs bumping by hand as before.🤖 Generated with Claude Code