From 2ad07f929ed12199d29d4da5154ed9691ff15a47 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 4 May 2026 06:12:39 -0300 Subject: [PATCH] refactor(superdoc): widen ExportParams.fieldsHighlightColor to string | null (SD-2828) The runtime defaults this field to `null` when the consumer omits it, forwards that `null` through to `Editor.exportDocx` (which already types it as `string | null`), and on through the converter. The previous public typedef narrowed to `string`, so consumers passing the runtime-equivalent `null` failed strict-mode typechecks. Adds a consumer fixture covering the three valid shapes (string, null, omitted) plus a strict `Equal` assertion so a future re-narrowing surfaces as a CI failure. --- packages/superdoc/src/core/SuperDoc.js | 2 +- packages/superdoc/src/core/types/index.ts | 10 ++++-- .../src/export-params-fields-highlight.ts | 34 +++++++++++++++++++ tests/consumer-typecheck/typecheck-matrix.mjs | 26 ++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/consumer-typecheck/src/export-params-fields-highlight.ts diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index 004a5f5def..07acea5a78 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -1743,7 +1743,7 @@ export class SuperDoc extends EventEmitter { /** * Export editors to DOCX format. - * @param {{ commentsType?: string, isFinalDoc?: boolean, fieldsHighlightColor?: string }} [options] + * @param {{ commentsType?: string, isFinalDoc?: boolean, fieldsHighlightColor?: string | null }} [options] * @returns {Promise>} */ async exportEditorsToDOCX({ commentsType, isFinalDoc, fieldsHighlightColor } = {}) { diff --git a/packages/superdoc/src/core/types/index.ts b/packages/superdoc/src/core/types/index.ts index 73d390f87f..517ac3643a 100644 --- a/packages/superdoc/src/core/types/index.ts +++ b/packages/superdoc/src/core/types/index.ts @@ -1160,8 +1160,14 @@ export interface ExportParams { isFinalDoc?: boolean; /** Auto-download or return blob. */ triggerDownload?: boolean; - /** Color for field highlights. */ - fieldsHighlightColor?: string; + /** + * Color for field highlights. The runtime defaults to `null` when no + * value is supplied (and forwards `null` through to the underlying + * editor export, which accepts `string | null`); the typedef accepts + * `null` explicitly so consumers can pass an explicit "no highlight" + * value without a typecheck failure. + */ + fieldsHighlightColor?: string | null; } /** Surface where the edit originated. */ diff --git a/tests/consumer-typecheck/src/export-params-fields-highlight.ts b/tests/consumer-typecheck/src/export-params-fields-highlight.ts new file mode 100644 index 0000000000..9d43f8f802 --- /dev/null +++ b/tests/consumer-typecheck/src/export-params-fields-highlight.ts @@ -0,0 +1,34 @@ +/** + * Consumer typecheck: `ExportParams.fieldsHighlightColor` accepts + * `string | null | undefined` (SD-2828). + * + * The runtime defaults the field to `null` when the consumer omits it, + * forwards that `null` through to `Editor.exportDocx` (which accepts + * `string | null`), and then on through the converter. Before this + * widening the public typedef declared `string` only, so consumers + * passing the runtime-equivalent `null` got a strict-mode typecheck + * failure on a value the runtime accepts. + * + * If a future change re-narrows this field to `string`, the assignments + * below stop compiling and CI fails. + */ +import type { ExportParams } from 'superdoc'; + +// Each of the three valid shapes a consumer can pass: explicit color, +// explicit "no highlight" (null), and omitted (undefined). The current +// runtime treats omitted and explicit-null the same. +const withColor: ExportParams = { fieldsHighlightColor: '#ff0000' }; +const withNull: ExportParams = { fieldsHighlightColor: null }; +const omitted: ExportParams = {}; + +// Strict type-equality assertion: a re-narrowing to `string` (or to +// `string | undefined` without `null`) would still leave the explicit +// color and omitted forms compiling, so plain assignments alone do not +// catch a regression. Pin the exact field type here. +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; +type AssertEqual = Equal extends true ? true : never; + +const _fieldsHighlightColorTypeIsExact: AssertEqual = + true; + +void [withColor, withNull, omitted, _fieldsHighlightColorTypeIsExact]; diff --git a/tests/consumer-typecheck/typecheck-matrix.mjs b/tests/consumer-typecheck/typecheck-matrix.mjs index fff5ba84ae..d4bd3eba80 100644 --- a/tests/consumer-typecheck/typecheck-matrix.mjs +++ b/tests/consumer-typecheck/typecheck-matrix.mjs @@ -520,6 +520,32 @@ const scenarios = [ files: ['src/search-match.ts'], mustPass: true, }, + + // SD-2828: `ExportParams.fieldsHighlightColor` accepts `string | null + // | undefined`. The runtime defaults the field to `null` when omitted + // and forwards that `null` straight through to `Editor.exportDocx` + // (which already types it as `string | null`). The previous public + // typedef narrowed to `string`, so consumers passing the + // runtime-equivalent `null` failed strict-mode typechecks. Pinned + // here so a future re-narrowing surfaces as a typecheck failure. + { + name: 'bundler / fieldsHighlightColor accepts null (SD-2828)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: true, + strict: true, + files: ['src/export-params-fields-highlight.ts'], + mustPass: true, + }, + { + name: 'node16 / fieldsHighlightColor accepts null (SD-2828)', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: true, + strict: true, + files: ['src/export-params-fields-highlight.ts'], + mustPass: true, + }, ]; const tscPath = join(__dirname, 'node_modules', '.bin', 'tsc');