diff --git a/.changeset/tender-planets-greet.md b/.changeset/tender-planets-greet.md new file mode 100644 index 0000000000..6430e9226c --- /dev/null +++ b/.changeset/tender-planets-greet.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Changes the PerseusWidgetsMap to be extensible so that widgets can be registered outside of Perseus and still have full type safety. diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index f8f4c96dd5..f16c1ecaf3 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -223,7 +223,9 @@ export type { PerseusRenderer, PerseusWidget, PerseusWidgetsMap, + PerseusWidgetTypes, MultiItem, + WidgetOptions, } from "./perseus-types"; export type {UserInputMap} from "./validation.types"; export type {Coord} from "./interactive2/types"; diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 46e318487f..d6b73bc3c3 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -11,74 +11,102 @@ export type Size = [number, number]; export type CollinearTuple = [vec.Vector2, vec.Vector2]; export type ShowSolutions = "all" | "selected" | "none"; +/** + * Our core set of Perseus widgets. + * + * This interface is the basis for "registering" all Perseus widget types. + * There should be one key/value pair for each supported widget. If you create + * a new widget, an entry should be added to this interface. Note that this + * only registers the widget options type, you'll also need to register the + * widget so that it's available at runtime (@see + * {@link file://./widgets.ts#registerWidget}). + * + * Importantly, the key should be the name that is used in widget IDs. For most + * widgets that is the same as the widget option's `type` field. In cases where + * a widget has been deprecated and replaced with the deprecated-standin + * widget, it should be the original widget type! + * + * If you define the widget outside of this package, you can still add the new + * widget to this interface by writing the following in that package that + * contains the widget. TypeScript will merge that definition of the + * `PerseusWidgets` with the one defined below. + * + * ```typescript + * declare module "@khanacademy/perseus" { + * interface PerseusWidgetTypes { + * // A new widget + * "new-awesomeness": MyAwesomeNewWidget; + * + * // A deprecated widget + * "super-old-widget": AutoCorrectWidget; + * } + * } + * + * // The new widget's options definition + * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>; + * + * // The deprecated widget's options definition + * type SuperOldWidget = WidgetOptions<'super-old-widget', object>; + * ``` + * + * This interface can be extended through the magic of TypeScript "Declaration + * merging". Specifically, we augment this module and extend this interface. + * + * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation} + */ +export interface PerseusWidgetTypes { + categorizer: CategorizerWidget; + "cs-program": CSProgramWidget; + definition: DefinitionWidget; + dropdown: DropdownWidget; + explanation: ExplanationWidget; + expression: ExpressionWidget; + grapher: GrapherWidget; + "graded-group-set": GradedGroupSetWidget; + "graded-group": GradedGroupWidget; + group: GroupWidget; + iframe: IFrameWidget; + image: ImageWidget; + "input-number": InputNumberWidget; + interaction: InteractionWidget; + "interactive-graph": InteractiveGraphWidget; + "label-image": LabelImageWidget; + matcher: MatcherWidget; + matrix: MatrixWidget; + measurer: MeasurerWidget; + "molecule-renderer": MoleculeRendererWidget; + "number-line": NumberLineWidget; + "numeric-input": NumericInputWidget; + orderer: OrdererWidget; + "passage-ref-target": RefTargetWidget; + "passage-ref": PassageRefWidget; + passage: PassageWidget; + "phet-simulation": PhetSimulationWidget; + "python-program": PythonProgramWidget; + plotter: PlotterWidget; + radio: RadioWidget; + sorter: SorterWidget; + table: TableWidget; + video: VideoWidget; + + // Deprecated widgets + sequence: AutoCorrectWidget; +} + +/** + * A map of widget IDs to widget options. This is most often used as the type + * for a set of widgets defined in a `PerseusItem` but can also be useful to + * represent a function parameter where only `widgets` from a `PerseusItem` are + * needed. Today Widget IDs are made up of the widget type and an incrementing + * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid + * reading/parsing the widget id to derive any information from it, except in + * the case of this map. + * + * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type + * by augmenting the PerseusWidgetTypes with new widget types! + */ export type PerseusWidgetsMap = { - [key in `categorizer ${number}`]: CategorizerWidget; -} & { - [key in `cs-program ${number}`]: CSProgramWidget; -} & { - [key in `definition ${number}`]: DefinitionWidget; -} & { - [key in `dropdown ${number}`]: DropdownWidget; -} & { - [key in `explanation ${number}`]: ExplanationWidget; -} & { - [key in `expression ${number}`]: ExpressionWidget; -} & { - [key in `grapher ${number}`]: GrapherWidget; -} & { - [key in `group ${number}`]: GroupWidget; -} & { - [key in `graded-group ${number}`]: GradedGroupWidget; -} & { - [key in `graded-group-set ${number}`]: GradedGroupSetWidget; -} & { - [key in `iframe ${number}`]: IFrameWidget; -} & { - [key in `image ${number}`]: ImageWidget; -} & { - [key in `input-number ${number}`]: InputNumberWidget; -} & { - [key in `interaction ${number}`]: InteractionWidget; -} & { - [key in `interactive-graph ${number}`]: InteractiveGraphWidget; -} & { - [key in `label-image ${number}`]: LabelImageWidget; -} & { - [key in `matcher ${number}`]: MatcherWidget; -} & { - [key in `matrix ${number}`]: MatrixWidget; -} & { - [key in `measurer ${number}`]: MeasurerWidget; -} & { - [key in `molecule-renderer ${number}`]: MoleculeRendererWidget; -} & { - [key in `number-line ${number}`]: NumberLineWidget; -} & { - [key in `numeric-input ${number}`]: NumericInputWidget; -} & { - [key in `orderer ${number}`]: OrdererWidget; -} & { - [key in `passage ${number}`]: PassageWidget; -} & { - [key in `passage-ref ${number}`]: PassageRefWidget; -} & { - [key in `passage-ref-target ${number}`]: PassageRefWidget; -} & { - [key in `phet-simulation ${number}`]: PhetSimulationWidget; -} & { - [key in `plotter ${number}`]: PlotterWidget; -} & { - [key in `python-program ${number}`]: PythonProgramWidget; -} & { - [key in `radio ${number}`]: RadioWidget; -} & { - [key in `sorter ${number}`]: SorterWidget; -} & { - [key in `table ${number}`]: TableWidget; -} & { - [key in `video ${number}`]: VideoWidget; -} & { - [key in `sequence ${number}`]: AutoCorrectWidget; + [Property in keyof PerseusWidgetTypes as `${Property} ${number}`]: PerseusWidgetTypes[Property]; }; /**