plotly.js
Version:
The open source javascript graphing library that powers plotly
200 lines (147 loc) • 7.7 kB
Markdown
# Converting Attribute Files to TypeScript
This is the **active workflow** for migrating Plotly's `attributes.js` files
to TypeScript. Attribute files are the source of truth for the runtime
schema; the schema in turn drives the generated public TypeScript types.
Conversion doesn't change that chain — it adds compile-time validation on
top of it.
## Why
Without `as const satisfies AttributeMap`, malformed attribute objects
(missing `valType`, typo'd `values` arrays, wrong `dflt` shape, etc.) only
fail when the runtime tries to use them. Converted attribute files catch
those structural mistakes at TypeScript-compile time, before they ever
reach the schema.
Conversion is also a small step toward typing the rest of the file (and
eventually the rest of the source tree) in TypeScript, but the immediate
value is the compile-time structural check.
## Recipe
Pick any `attributes.js` file. The recipe is the same for every one. A
small file takes ~10 minutes; a complex trace might take an hour.
### 1. Rename and stub the imports
Rename `src/<path>/attributes.js` → `src/<path>/attributes.ts`.
At the top of the file, add:
```ts
import type { AttributeMap } from '../../types/lib/attributes';
// (adjust the relative path so it points at src/types/lib/attributes)
```
### 2. Convert the export
Replace `module.exports = { ... };` with:
```ts
const attributes = {
// ... existing attribute definitions go here
} as const satisfies AttributeMap;
export default attributes;
```
Two things to notice:
- **`as const satisfies AttributeMap`** — `as const` preserves literal types
like `values: ['v', 'h']`; `satisfies AttributeMap` validates structure
without widening.
- **`export default`** — runtime consumers (`require('./attributes').default`)
get the object.
### 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 `as const`, `values` widens to `string[]` and `dflt` is no longer
constrained to be one of `values`. `as const` is what lets `AttributeMap`
catch `dflt`-not-in-`values` typos at compile time.
### 4. Update consumers' `require()` calls
Find every JS file that `require('./attributes')` (relative to the converted
file) and update to `.default`:
```js
// Before
var attributes = require('./attributes');
// After
var attributes = require('./attributes').default;
```
ESBuild handles the runtime; this update is just for the JS-level CommonJS
interop pattern the project already uses for converted files.
### 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.
### 6. Verify
```bash
npm run typecheck # zero errors
npm run schema-typegen-diff-check # regen + check test/plot-schema.json
# and src/types/generated/ are unchanged
```
The `schema-typegen-diff-check` script regenerates both the runtime schema
and the generated `.d.ts`, then `git diff --exit-code`s them. A correct
conversion produces a byte-identical schema; CI fails otherwise. This is
the conversion's safety net — if either file diffs after the conversion,
the attribute object's runtime shape changed (most often a missed
`as const` or a typo). Compare character-by-character with the original
`.js` file.
### 7. Commit
```bash
git add src/<path>/attributes.ts \
src/<path>/index.js src/<path>/defaults.js # (whichever consumers you updated)
git commit -m "Convert <component> attributes to TypeScript"
```
The conversion is a single self-contained commit per file. There's
nothing to commit under `src/types/` for a correct conversion — the
generated types are byte-identical (which is exactly what
`schema-typegen-diff-check` confirmed in step 6).
## Worked example: modebar
See [`src/components/modebar/attributes.ts`](../components/modebar/attributes.ts)
for the canonical example. The full conversion changed:
- `src/components/modebar/attributes.js` → `src/components/modebar/attributes.ts`
(with `as const satisfies AttributeMap`)
- `.default` added to `require('./attributes')` in `index.js` and `defaults.js`
Note: Consumer-facing types for modebar (and all other layout components)
are generated from `plot-schema.json` by `tasks/generate_schema_types.mjs`,
not from the individual `attributes.ts` files. The `attributes.ts` conversion
still adds value by type-checking the source attribute definitions against
`AttributeMap`.
## What if I need a type the schema doesn't describe well?
The schema-generated types are authoritative for everything in
`plot-schema.json`. If something is missing or too loose:
1. **Compare**. Look at the schema-generated type in `schema.d.ts`.
2. **If schema is too loose** (e.g. `string` where the schema should have
a typed union of valid values, or `any` where a proper shape could be
described), fix it at the JS attribute source so every language port
benefits — not via a generator-side override.
3. **If the field is an internal runtime artifact** (not in the user-facing
schema), add it to the appropriate `.internal.d.ts` file. For trace
internal fields, that's `FullDataInternals` inside
`core/data.internal.d.ts` (which `FullData` intersects with `Data`).
For layout internal fields, it's `FullLayout` in `layout.internal.d.ts`.
4. **If the schema has fields and a generator-side override is the only
path** (e.g. recursive references like `Frame.data` pointing back to the
trace-data union), use the `fieldOverrides` parameter on
`attrsToProperties` in the generator. See GENERATOR.md.
## Schema-generated types
All trace data interfaces, layout component interfaces, and the Layout
interface itself are generated from `plot-schema.json` by
`tasks/generate_schema_types.mjs` (run via `npm run schema`). Individual
trace and layout `attributes.js` files do **not** need to be converted
for their types to appear in the public API — the schema generator
covers them automatically.
## Order of conversion (for parallel work)
Pick from this priority list. Lower-numbered items are smaller / simpler.
### Tier 1: small components (good first conversions)
- `src/components/modebar/attributes.js` — **DONE** (canonical example)
- `src/components/rangeslider/attributes.js` — small, self-contained
- `src/plots/gl3d/layout/attributes.js` — tiny (one `subplotid`)
- `src/plots/cartesian/attributes.js` — small
- `src/components/fx/attributes.js` — uses helpers (`fontAttrs`, shared sub-objects)
> Note: `src/components/color/attributes.js` is named like an attribute file
> but actually just exports color constants. It doesn't follow the schema
> pattern and shouldn't be converted with this recipe.
### Tier 2: medium components
- Sliders, updatemenus, rangeselector, colorbar attribute files
### Tier 3: layout
- Layout itself
## Working in parallel
Multiple converters can work on different attribute files in parallel.
Each conversion is self-contained within one component's directory plus
its direct `require()`-callers, so merge conflicts are rare.