build: Enable TypeScript type checking#7680
Conversation
| var constants = require('./constants'); | ||
| var overrideAll = require('../../plot_api/edit_types').overrideAll; | ||
| var sortObjectKeys = require('../../lib/sort_object_keys'); | ||
| var sortObjectKeys = require('../../lib/sort_object_keys').default; |
There was a problem hiding this comment.
@camdecoster Is it worth considering moving away from default exports entirely as part of this transition?
There was a problem hiding this comment.
It only matters when one mixes CJS and ESM. That said, I don't think that we can avoid that for a long time. I suppose we could make just pick a direction to go and standardize. I could go either way, but the esbuild docs make it clear that they don't like default exports.
| fillcolor?: string; | ||
| line?: Partial<Line>; | ||
| marker?: Partial<Marker>; | ||
| mode?: 'lines' | 'markers' | 'lines+markers' | 'none' | 'text' | 'lines+text' | 'markers+text' | 'lines+markers+text'; |
There was a problem hiding this comment.
Does TypeScript have any support for 'flag list'-type string values such as this, where the string may consist of any number of a fixed set of values joined by a delimiter? I suppose not as it's fairly custom.
Otherwise could we use a custom type or a custom function to generate these lists of allowed values based on the flags?
There was a problem hiding this comment.
Yes, it's called a union type. What's shown on 133 is an example of that (though it's only used on that line). If we needed that type elsewhere, we could define it separately and reference it within the ScatterTrace type like this:
type Mode = 'lines' | 'markers' | 'lines+markers' | 'none' | 'text' | 'lines+text' | 'markers+text' | 'lines+markers+text';
export interface ScatterTrace extends TraceBase {
...,
mode: Mode;
...,
}There was a problem hiding this comment.
Yeah I guess I'm imagining something like a helper function which looks like flagList(flaglistVals, otherVals) such that
flaglistVals(['lines', 'markers', 'text'], ['none'])
returns
'lines' | 'markers' | 'lines+markers' | 'none' | 'text' | 'lines+text' | 'markers+text' | 'lines+markers+text';`
| * Use specific trace types when available | ||
| */ | ||
| export interface GenericTrace extends TraceBase { | ||
| x?: any[]; |
There was a problem hiding this comment.
I'm not sure there are any traces where x/y/z are allowed to be a type other than number[] | string[] but I could be wrong about that.
There was a problem hiding this comment.
That's why it's permissive here. Once we become sure, we can change this as you suggest.
There was a problem hiding this comment.
In this case maybe it would be easier to start stricter and loosen if needed?
|
@camdecoster Can you add a type-check step to the CI? |
| yaxis?: Partial<LayoutAxis>; | ||
|
|
||
| // Multiple axes support (xaxis2, yaxis3, etc.) | ||
| [key: string]: any; |
There was a problem hiding this comment.
Can we tighten this layout spec here to match the plot schema?
|
@camdecoster Some initial thoughts on organization of the types: For types corresponding directly to the schema:
|
codeCraft-Ritik
left a comment
There was a problem hiding this comment.
Great work! The implementation is clean and easy to understand
2439dad to
de168ca
Compare
| ### 3. Fix array literals and string literals | ||
|
|
||
| Anywhere an attribute uses a literal-array of options, add `as const`: | ||
|
|
||
| ```ts | ||
| // Before | ||
| values: ['v', 'h'], | ||
|
|
||
| // After | ||
| values: ['v', 'h'] as const, | ||
| ``` | ||
|
|
||
| Without this, TypeScript widens to `string[]` and you lose the union. |
There was a problem hiding this comment.
is this step still required now that the types are not generated directly from the attributes?
| ### 5. Verify the schema generator covers the type | ||
|
|
||
| Consumer-facing types for traces and layout components are generated from | ||
| `plot-schema.json` by `tasks/generate_schema_types.mjs`. After converting | ||
| an `attributes.ts` file, verify the corresponding type already exists in | ||
| `src/types/generated/schema.d.ts`. If it does, no further action is needed | ||
| for the type — the conversion's main value is type-checking the source. | ||
|
|
||
| If the schema-generated type is missing properties that the hand-written | ||
| type had, those properties are likely runtime-only internal state and | ||
| should be added to the corresponding `Full*` interface instead. |
There was a problem hiding this comment.
This step isn't needed anymore either, right?
Maybe could be replaced with something like "If you did everything right above, the plot schema should not change. Run npm run schema and verify that there are no changes to the file test/plot-schema.json."
There was a problem hiding this comment.
Actually, I guess that part is already covered by the next step.
| - `.default` added to `require('./attributes')` in `index.js` and `defaults.js` | ||
|
|
||
| Note: Consumer-facing types for modebar (and all other layout components) | ||
| are now generated from `plot-schema.json` by `tasks/generate_schema_types.mjs`, |
There was a problem hiding this comment.
| are now generated from `plot-schema.json` by `tasks/generate_schema_types.mjs`, | |
| are generated from `plot-schema.json` by `tasks/generate_schema_types.mjs`, |
|
|
||
| Schema output verified byte-identical (2547 bytes) before and after the | ||
| conversion. |
There was a problem hiding this comment.
| Schema output verified byte-identical (2547 bytes) before and after the | |
| conversion. |
| ## What stays hand-written | ||
|
|
||
| The schema does not describe these — they remain in `src/types/`: | ||
|
|
||
| - **Events** (`PlotMouseEvent`, `LegendClickEvent`, etc.) | ||
| - **Public API function signatures** (`Plotly.newPlot`, `relayout`, ...) | ||
| - **Internal types** (`FullLayout._modules`, `GraphDiv._fullData`, ...) | ||
| - **Utility types** (`Color`, `Datum`, `TypedArray`, `MarkerSymbol`, etc.) — | ||
| these are the primitives the generator's emitted types reference. | ||
|
|
||
| If you find yourself converting one of these, stop and ask. |
There was a problem hiding this comment.
This is a duplicate of What's hand-written and stays that way in ARCHITECTURE.md and doesn't really make sense here anymore.
| ## What stays hand-written | |
| The schema does not describe these — they remain in `src/types/`: | |
| - **Events** (`PlotMouseEvent`, `LegendClickEvent`, etc.) | |
| - **Public API function signatures** (`Plotly.newPlot`, `relayout`, ...) | |
| - **Internal types** (`FullLayout._modules`, `GraphDiv._fullData`, ...) | |
| - **Utility types** (`Color`, `Datum`, `TypedArray`, `MarkerSymbol`, etc.) — | |
| these are the primitives the generator's emitted types reference. | |
| If you find yourself converting one of these, stop and ask. |
|
|
||
| ## Schema-generated types | ||
|
|
||
| All 49 trace data interfaces, layout component interfaces, and the Layout |
There was a problem hiding this comment.
| All 49 trace data interfaces, layout component interfaces, and the Layout | |
| All trace data interfaces, layout component interfaces, and the Layout |
|
|
||
| Converting an `attributes.js` file to TypeScript is still valuable because | ||
| it type-checks the source definitions against `AttributeMap`, catching | ||
| typos and structural errors at compile time. |
There was a problem hiding this comment.
duplicate of lines 139-141 above
| Converting an `attributes.js` file to TypeScript is still valuable because | |
| it type-checks the source definitions against `AttributeMap`, catching | |
| typos and structural errors at compile time. |
There was a problem hiding this comment.
Since the attributes files aren't directly contributing to the type generation anymore, this file could probably be made more concise. Not blocking for merge though.
| - Common enum aliases (Calendar, Dash, AxisType, PatternShape, XRef, YRef, | ||
| TransitionEasing, PlotType) | ||
| - Shared sub-interfaces (Font, ColorBar, HoverLabel, etc.) | ||
| - 49 per-trace data interfaces (BarData, ScatterData, IndicatorData, ...) |
There was a problem hiding this comment.
| - 49 per-trace data interfaces (BarData, ScatterData, IndicatorData, ...) | |
| - Data interfaces for each trace type (BarData, ScatterData, IndicatorData, etc.) |
| // PascalCase type name and a `match(key, path, values)` predicate. The first | ||
| // enumerated attribute whose match returns true defines the alias's values. |
There was a problem hiding this comment.
| // PascalCase type name and a `match(key, path, values)` predicate. The first | |
| // enumerated attribute whose match returns true defines the alias's values. | |
| // PascalCase type name and a `match(key, path, values)` predicate. If multiple | |
| // enumerated attributes match the predicate, the one with the largest set of values | |
| // is chosen. |
| Containers that appear at least `MIN_OCCURRENCES` (= 5) times AND have at | ||
| least `MIN_PROPERTIES` (= 4) properties become shared interfaces (Font, |
There was a problem hiding this comment.
| Containers that appear at least `MIN_OCCURRENCES` (= 5) times AND have at | |
| least `MIN_PROPERTIES` (= 4) properties become shared interfaces (Font, | |
| Containers that appear at least `MIN_OCCURRENCES` times AND have at | |
| least `MIN_PROPERTIES` properties become shared interfaces (Font, |
|
|
||
| Regenerate with `npm run schema`. |
There was a problem hiding this comment.
| Regenerate with `npm run schema`. |
| The wildcard is load-bearing: it removes the maintenance burden of keeping | ||
| `lib/index.d.ts` in sync with new schema additions. If anyone ever swaps | ||
| it for an explicit allowlist, restore the per-name re-export verifier | ||
| that previously lived in `tasks/schema.mjs` (see git history) — otherwise | ||
| new generated types will silently fail to surface in the public API. |
There was a problem hiding this comment.
Is this paragraph needed? I don't think it's likely someone would decide to remove the wildcard.
| The wildcard is load-bearing: it removes the maintenance burden of keeping | |
| `lib/index.d.ts` in sync with new schema additions. If anyone ever swaps | |
| it for an explicit allowlist, restore the per-name re-export verifier | |
| that previously lived in `tasks/schema.mjs` (see git history) — otherwise | |
| new generated types will silently fail to surface in the public API. |
| `git diff --exit-code`. If either differs, exit code 1 — meaning either a | ||
| developer changed the source schema but didn't commit the regenerated | ||
| artifacts, or an attribute-file conversion silently altered the runtime | ||
| schema and the change wasn't intentionally committed. |
There was a problem hiding this comment.
| `git diff --exit-code`. If either differs, exit code 1 — meaning either a | |
| developer changed the source schema but didn't commit the regenerated | |
| artifacts, or an attribute-file conversion silently altered the runtime | |
| schema and the change wasn't intentionally committed. | |
| `git diff --exit-code`. If either differs, the command fails with exit code 1 | |
| and outputs the diff to the console. |
| - TypeScript build infrastructure: ✅ done | ||
| - Public type surface in `src/types/`: ✅ done | ||
| - `AttributeMap` validation machinery: ✅ done | ||
| - **Schema-based type generator**: ✅ done — all 49 trace types + layout + shared interfaces |
There was a problem hiding this comment.
| - **Schema-based type generator**: ✅ done — all 49 trace types + layout + shared interfaces | |
| - **Schema-based type generator**: ✅ done — all trace types + layout + shared interfaces |
| - CI gates (`typecheck` + `schema-typegen-diff-check`): ✅ done | ||
| - First attribute file converted (modebar): ✅ done | ||
| - Conversion of remaining component attribute files: 🚧 in progress | ||
|
|
There was a problem hiding this comment.
The rest of the .js files in the repo which are not attributes files should also ideally be converted eventually, right? Add that to the list?
|
|
||
| - Common enum aliases (Calendar, Dash, AxisType, PatternShape, XRef, YRef, | ||
| TransitionEasing, PlotType) | ||
| - All 49 per-trace data interfaces (BarData, ScatterData, IndicatorData, ...) |
There was a problem hiding this comment.
| - All 49 per-trace data interfaces (BarData, ScatterData, IndicatorData, ...) | |
| - Data interfaces for each trace type (BarData, ScatterData, IndicatorData, etc.) |
| Quick reference for the TypeScript toolchain in plotly.js. | ||
|
|
||
| ## What's installed | ||
|
|
There was a problem hiding this comment.
| The following dev dependencies are used for maintaining plotly.js types: | |
| - **TypeScript** (`typescript ^5.9.3`) — type checker only, never emits JS | ||
| - **ts-node** (`^10.9.2`) — runs TS scripts directly (build helpers) | ||
| - **@types/node**, **@types/d3** — third-party type definitions | ||
| - esbuild handles `.ts` natively for bundling — no plugins needed |
There was a problem hiding this comment.
| - **TypeScript** (`typescript ^5.9.3`) — type checker only, never emits JS | |
| - **ts-node** (`^10.9.2`) — runs TS scripts directly (build helpers) | |
| - **@types/node**, **@types/d3** — third-party type definitions | |
| - esbuild handles `.ts` natively for bundling — no plugins needed | |
| - `typescript` — used for type-checking only | |
| - `ts-node` — used for running TS scripts (build helpers) | |
| - `@types/node`, `@types/d3` — provide third-party type definitions | |
| Note: esbuild handles `.ts` files natively for bundling, so no extra plugins are needed for the bundling process. | |
| `tsconfig.json` sets `noEmit: true` so that tsc never writes files. esbuild is the build system; tsc is the verifier. |
| ## Two tools, two jobs | ||
|
|
||
| | Tool | Job | | ||
| |---|---| | ||
| | **esbuild** | Bundle for production. Strips types, transpiles to ES2016, emits `dist/plotly.js`. Fast (~450ms full build). Does **not** check types. | | ||
| | **tsc** | Type-check only. Reads `.ts` and `.js` (with `allowJs: true`), reports errors, no output. Slower (~2-5s) but catches bugs. | | ||
|
|
||
| `tsconfig.json` sets `noEmit: true` so tsc never writes files. esbuild is the build system; tsc is the verifier. |
There was a problem hiding this comment.
| ## Two tools, two jobs | |
| | Tool | Job | | |
| |---|---| | |
| | **esbuild** | Bundle for production. Strips types, transpiles to ES2016, emits `dist/plotly.js`. Fast (~450ms full build). Does **not** check types. | | |
| | **tsc** | Type-check only. Reads `.ts` and `.js` (with `allowJs: true`), reports errors, no output. Slower (~2-5s) but catches bugs. | | |
| `tsconfig.json` sets `noEmit: true` so tsc never writes files. esbuild is the build system; tsc is the verifier. |
| npm run typecheck # tsc --noEmit, errors reported, no output | ||
| npm run typecheck-watch # incremental rechecking on change | ||
|
|
||
| npm run schema # rebuild test/plot-schema.json + regenerate types |
There was a problem hiding this comment.
| npm run schema # rebuild test/plot-schema.json + regenerate types | |
| npm run schema # rebuild test/plot-schema.json + regenerate types under src/types/generated/ |
| npm run typecheck-watch # incremental rechecking on change | ||
|
|
||
| npm run schema # rebuild test/plot-schema.json + regenerate types | ||
| npm run schema-typegen-diff-check # regenerate + verify no uncommitted drift in test/plot-schema.json or schema.d.ts |
There was a problem hiding this comment.
| npm run schema-typegen-diff-check # regenerate + verify no uncommitted drift in test/plot-schema.json or schema.d.ts | |
| npm run schema-typegen-diff-check # regenerate + verify no changes to test/plot-schema.json or src/types/generated/schema.d.ts |
| npm run bundle # esbuild → dist/plotly.js | ||
| npm run build # full production build |
There was a problem hiding this comment.
| npm run bundle # esbuild → dist/plotly.js | |
| npm run build # full production build | |
| npm run build # full production build (regenerate all files under `dist/`) |
| ## Performance | ||
|
|
||
| For a codebase of ~750 source files: | ||
|
|
||
| | Operation | Time | | ||
| |---|---| | ||
| | `tsc --noEmit` cold | ~2-5s | | ||
| | `tsc --noEmit --watch` incremental | ~100ms | | ||
| | esbuild full bundle | ~450ms | |
There was a problem hiding this comment.
Not sure this section is needed in SETUP.md.
| ## Performance | |
| For a codebase of ~750 source files: | |
| | Operation | Time | | |
| |---|---| | |
| | `tsc --noEmit` cold | ~2-5s | | |
| | `tsc --noEmit --watch` incremental | ~100ms | | |
| | esbuild full bundle | ~450ms | |
| // generate TypeScript types from the schema — traces, layout, common enum | ||
| // aliases, animation/frame/config interfaces, and an `_internal` namespace. | ||
| // | ||
| // New schema-derived types automatically reach `plotly.js` consumers because | ||
| // `lib/index.d.ts` uses `export type * from '../src/types/generated/schema'`. | ||
| // If you ever swap that wildcard for an explicit allowlist, restore the | ||
| // per-name re-export verifier that lived here (see git history) — otherwise | ||
| // new types will silently fail to surface in the public API. |
There was a problem hiding this comment.
| // generate TypeScript types from the schema — traces, layout, common enum | |
| // aliases, animation/frame/config interfaces, and an `_internal` namespace. | |
| // | |
| // New schema-derived types automatically reach `plotly.js` consumers because | |
| // `lib/index.d.ts` uses `export type * from '../src/types/generated/schema'`. | |
| // If you ever swap that wildcard for an explicit allowlist, restore the | |
| // per-name re-export verifier that lived here (see git history) — otherwise | |
| // new types will silently fail to surface in the public API. | |
| // generate TypeScript types from the schema | |
| // and write to `src/types/generated/schema.d.ts` |
| "outDir": "./dist", | ||
| "noEmit": true, | ||
|
|
||
| // Type checking - start strict, loosen if necessary |
There was a problem hiding this comment.
| // Type checking - start strict, loosen if necessary |
| "declaration": false, | ||
| "declarationMap": false, | ||
| "sourceMap": true, | ||
| "outDir": "./dist", |
There was a problem hiding this comment.
Is this needed if there's no output?
There was a problem hiding this comment.
Oh never mind, I guess this config file is used by esbuild as well?
| } | ||
|
|
||
| // PlotType — derived from the list of trace names, not from an attribute. | ||
| found.set('PlotType', { values: Object.keys(schema.traces).sort() }); |
There was a problem hiding this comment.
This really should be called TraceType... the question is, is it worth breaking correspondence with DefinitelyTyped to use the semantically correct type name?
|
@camdecoster I think it would be useful to add guidance for converting non- |
emilykl
left a comment
There was a problem hiding this comment.
Left a bunch of nitpicky comments but big picture LGTM.
Description
Enable TypeScript type checking.
Closes #7678.
Changes
attributes filesschemaTesting
npm run buildlocally to check that everything is bundled properly by esbuildnpm run schemato recreate the generated typesnpm run typecheckto perform a type check on the source code. There should be no errors:Notes
npm run typecheck. VS Code will also provide feedback as you're editing files.TS is not emitting files/types right now. esbuild handles converting the TS so the TS compiler won't need to do that. It would be valuable to have the compiler write the type definition files in the future.