From f71f9652bc79b6e414bbe423fd7542ec188c3875 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Fri, 10 Apr 2026 10:18:56 -0700 Subject: [PATCH 1/2] fix(types): chain commands return ChainableCommandObject, not boolean (SD-2334) ChainableCommandObject used KnownChainedCommands (empty for npm consumers since module augmentation doesn't survive the package boundary) plus a Record index signature that conflicted with run: () => boolean. This caused TypeScript to infer intermediate chain methods could return boolean, breaking chains like chain().setTextSelection(...).setMark(...).run(). Apply the same fix used for EditorCommands: compose AllChainedCommands from direct imports of each command interface, transformed via Chainified to return ChainableCommandObject. Remove the Record fallback. Same treatment for CanObject to prevent can().chain() type conflicts. --- .../editors/v1/core/types/ChainedCommands.ts | 63 ++++++++++++++----- .../src/customer-scenario.ts | 9 +++ 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts index 55f455d602..eae49fa850 100644 --- a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts +++ b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts @@ -47,45 +47,76 @@ type KnownCommandRecord = { [K in keyof RegisteredCommands]: CommandForKey; }; +/** + * Transforms a command interface so every method returns ChainableCommandObject + * instead of boolean, preserving parameter types. + */ +type Chainified = { + [K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K]; +}; + +/** + * All chain-typed commands composed from explicit imports. + * Mirrors EditorCommands but with each method returning ChainableCommandObject. + * + * Module augmentation (CoreCommandMap/ExtensionCommandMap) doesn't survive + * the npm boundary, so we use direct imports — same pattern as EditorCommands. + */ +type AllChainedCommands = Chainified & + Chainified & + Chainified & + Chainified & + Chainified & + Chainified & + Chainified & + Chainified & + Chainified & + Chainified; + +/** + * All can-check commands composed from explicit imports. + * Mirrors EditorCommands with original return types for availability checks. + */ +type AllCanCommands = CoreCommandSignatures & + CommentCommands & + FormattingCommandAugmentations & + HistoryLinkTableCommandAugmentations & + SpecializedCommandAugmentations & + ParagraphCommands & + BlockNodeCommands & + ImageCommands & + MiscellaneousCommands & + TrackChangesCommands; + /** * A chainable version of an editor command keyed by command name. */ export type ChainedCommand = (...args: CommandArgs) => ChainableCommandObject; -type KnownChainedCommands = { - [K in keyof RegisteredCommands]: (...args: CommandArgs) => ChainableCommandObject; -}; - /** * Chainable command object returned by `createChain`. - * Has dynamic keys (one per command) and a `run()` method. + * Only `run()` returns boolean — all other methods return ChainableCommandObject. */ export type ChainableCommandObject = { run: () => boolean; -} & KnownChainedCommands & - Record ChainableCommandObject>; +} & AllChainedCommands; /** * A command that can be checked for availability. */ export type CanCommand = (...args: CommandArgs) => CommandResult; -type KnownCanCommands = { - [K in keyof RegisteredCommands]: (...args: CommandArgs) => CommandResult; -}; - /** * Map of commands that can be checked. */ export type CanCommands = Record; /** - * Object returned by `createCan`: dynamic boolean commands + a `chain()` helper. + * Object returned by `createCan`: typed boolean commands + a `chain()` helper. */ -export type CanObject = KnownCanCommands & - Record & { - chain: () => ChainableCommandObject; - }; +export type CanObject = AllCanCommands & { + chain: () => ChainableCommandObject; +}; /** * Core editor commands available on all instances. diff --git a/tests/consumer-typecheck/src/customer-scenario.ts b/tests/consumer-typecheck/src/customer-scenario.ts index 7e224bbcb0..f2accc2fb0 100644 --- a/tests/consumer-typecheck/src/customer-scenario.ts +++ b/tests/consumer-typecheck/src/customer-scenario.ts @@ -270,6 +270,15 @@ function testEditorCommands(editor: Editor) { // Chain API editor.chain().toggleBold().toggleItalic().run(); + + // SD-2334: Chain intermediate methods must return ChainableCommandObject, not boolean. + // Reproduces IT-344 (Ontra): chain().setTextSelection(...).setMark(...).run() + const chainResult: ChainableCommandObject = editor.chain().setTextSelection({ from: 0, to: 5 }); + const runResult: boolean = editor.chain().setTextSelection({ from: 0, to: 5 }).setMark('bold').run(); + + // SD-2334: can().chain() must return ChainableCommandObject, not boolean + const canChain: ChainableCommandObject = editor.can().chain(); + const canChainRun: boolean = editor.can().chain().toggleBold().run(); } function testPresentationEditorCommands(pe: PresentationEditor) { From 2e5ff6de15a2be56fb622698782c2e6883dd46b1 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Fri, 10 Apr 2026 10:37:23 -0700 Subject: [PATCH 2/2] refactor(types): extract AllCommandSignatures, restore augmentation support Address review feedback: - Extract shared AllCommandSignatures base type used by EditorCommands, ChainableCommandObject, and CanObject (eliminates triple-maintained 10-interface intersection) - Add AugmentedChainedCommands/AugmentedCanCommands so consumers who extend ExtensionCommandMap via module augmentation still get their custom commands on chain() and can() --- .../editors/v1/core/types/ChainedCommands.ts | 93 +++++++++---------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts index eae49fa850..bc87cd5c1b 100644 --- a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts +++ b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts @@ -48,36 +48,12 @@ type KnownCommandRecord = { }; /** - * Transforms a command interface so every method returns ChainableCommandObject - * instead of boolean, preserving parameter types. - */ -type Chainified = { - [K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K]; -}; - -/** - * All chain-typed commands composed from explicit imports. - * Mirrors EditorCommands but with each method returning ChainableCommandObject. - * - * Module augmentation (CoreCommandMap/ExtensionCommandMap) doesn't survive - * the npm boundary, so we use direct imports — same pattern as EditorCommands. + * Union of all command interfaces via explicit imports. + * Module augmentation doesn't survive the npm boundary, so this is the + * single source of truth for the built-in command surface. Used by + * EditorCommands, ChainableCommandObject, and CanObject. */ -type AllChainedCommands = Chainified & - Chainified & - Chainified & - Chainified & - Chainified & - Chainified & - Chainified & - Chainified & - Chainified & - Chainified; - -/** - * All can-check commands composed from explicit imports. - * Mirrors EditorCommands with original return types for availability checks. - */ -type AllCanCommands = CoreCommandSignatures & +type AllCommandSignatures = CoreCommandSignatures & CommentCommands & FormattingCommandAugmentations & HistoryLinkTableCommandAugmentations & @@ -88,6 +64,29 @@ type AllCanCommands = CoreCommandSignatures & MiscellaneousCommands & TrackChangesCommands; +/** + * Transforms a command interface so every method returns ChainableCommandObject + * instead of boolean, preserving parameter types. + */ +type Chainified = { + [K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K]; +}; + +/** + * Commands from module augmentation, transformed for chaining. + * Empty for npm consumers (augmentation doesn't survive the boundary), + * but consumers who augment ExtensionCommandMap get their custom commands + * on chain() for free. + */ +type AugmentedChainedCommands = { + [K in keyof RegisteredCommands]: (...args: CommandArgs) => ChainableCommandObject; +}; + +/** Same as AugmentedChainedCommands but with original return types for can(). */ +type AugmentedCanCommands = { + [K in keyof RegisteredCommands]: (...args: CommandArgs) => CommandResult; +}; + /** * A chainable version of an editor command keyed by command name. */ @@ -96,10 +95,14 @@ export type ChainedCommand = (...args: CommandArgs /** * Chainable command object returned by `createChain`. * Only `run()` returns boolean — all other methods return ChainableCommandObject. + * + * Includes AugmentedChainedCommands so consumers who extend ExtensionCommandMap + * via module augmentation get their custom commands on chain() automatically. */ export type ChainableCommandObject = { run: () => boolean; -} & AllChainedCommands; +} & Chainified & + AugmentedChainedCommands; /** * A command that can be checked for availability. @@ -113,10 +116,14 @@ export type CanCommands = Record; /** * Object returned by `createCan`: typed boolean commands + a `chain()` helper. + * + * Includes AugmentedCanCommands so consumers who extend ExtensionCommandMap + * via module augmentation get their custom commands on can() automatically. */ -export type CanObject = AllCanCommands & { - chain: () => ChainableCommandObject; -}; +export type CanObject = AllCommandSignatures & + AugmentedCanCommands & { + chain: () => ChainableCommandObject; + }; /** * Core editor commands available on all instances. @@ -131,23 +138,11 @@ export type ExtensionCommands = Pick fallback allows dynamic/plugin commands. + * Composed from AllCommandSignatures (explicit imports) for reliable + * cross-package typing, plus CoreCommands/ExtensionCommands (module + * augmentation) and a Record fallback for dynamic/plugin commands. */ -export type EditorCommands = CoreCommands & - ExtensionCommands & - CoreCommandSignatures & - CommentCommands & - FormattingCommandAugmentations & - HistoryLinkTableCommandAugmentations & - SpecializedCommandAugmentations & - ParagraphCommands & - BlockNodeCommands & - ImageCommands & - MiscellaneousCommands & - TrackChangesCommands & - Record; +export type EditorCommands = CoreCommands & ExtensionCommands & AllCommandSignatures & Record; /** * Command props made available to every command handler.