Skip to content

Commit 8f55848

Browse files
authored
fix(types): chain commands return ChainableCommandObject, not boolean (SD-2334) (#2773)
* 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<string, ...> 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<T> to return ChainableCommandObject. Remove the Record<string, ...> fallback. Same treatment for CanObject to prevent can().chain() type conflicts. * 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()
1 parent 9d03f01 commit 8f55848

File tree

2 files changed

+64
-29
lines changed

2 files changed

+64
-29
lines changed

packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,42 +48,80 @@ type KnownCommandRecord = {
4848
};
4949

5050
/**
51-
* A chainable version of an editor command keyed by command name.
51+
* Union of all command interfaces via explicit imports.
52+
* Module augmentation doesn't survive the npm boundary, so this is the
53+
* single source of truth for the built-in command surface. Used by
54+
* EditorCommands, ChainableCommandObject, and CanObject.
5255
*/
53-
export type ChainedCommand<K extends string = string> = (...args: CommandArgs<K>) => ChainableCommandObject;
56+
type AllCommandSignatures = CoreCommandSignatures &
57+
CommentCommands &
58+
FormattingCommandAugmentations &
59+
HistoryLinkTableCommandAugmentations &
60+
SpecializedCommandAugmentations &
61+
ParagraphCommands &
62+
BlockNodeCommands &
63+
ImageCommands &
64+
MiscellaneousCommands &
65+
TrackChangesCommands;
5466

55-
type KnownChainedCommands = {
67+
/**
68+
* Transforms a command interface so every method returns ChainableCommandObject
69+
* instead of boolean, preserving parameter types.
70+
*/
71+
type Chainified<T> = {
72+
[K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K];
73+
};
74+
75+
/**
76+
* Commands from module augmentation, transformed for chaining.
77+
* Empty for npm consumers (augmentation doesn't survive the boundary),
78+
* but consumers who augment ExtensionCommandMap get their custom commands
79+
* on chain() for free.
80+
*/
81+
type AugmentedChainedCommands = {
5682
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => ChainableCommandObject;
5783
};
5884

85+
/** Same as AugmentedChainedCommands but with original return types for can(). */
86+
type AugmentedCanCommands = {
87+
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => CommandResult<K>;
88+
};
89+
90+
/**
91+
* A chainable version of an editor command keyed by command name.
92+
*/
93+
export type ChainedCommand<K extends string = string> = (...args: CommandArgs<K>) => ChainableCommandObject;
94+
5995
/**
6096
* Chainable command object returned by `createChain`.
61-
* Has dynamic keys (one per command) and a `run()` method.
97+
* Only `run()` returns boolean — all other methods return ChainableCommandObject.
98+
*
99+
* Includes AugmentedChainedCommands so consumers who extend ExtensionCommandMap
100+
* via module augmentation get their custom commands on chain() automatically.
62101
*/
63102
export type ChainableCommandObject = {
64103
run: () => boolean;
65-
} & KnownChainedCommands &
66-
Record<string, (...args: unknown[]) => ChainableCommandObject>;
104+
} & Chainified<AllCommandSignatures> &
105+
AugmentedChainedCommands;
67106

68107
/**
69108
* A command that can be checked for availability.
70109
*/
71110
export type CanCommand<K extends string = string> = (...args: CommandArgs<K>) => CommandResult<K>;
72111

73-
type KnownCanCommands = {
74-
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => CommandResult<K>;
75-
};
76-
77112
/**
78113
* Map of commands that can be checked.
79114
*/
80115
export type CanCommands = Record<string, CanCommand>;
81116

82117
/**
83-
* Object returned by `createCan`: dynamic boolean commands + a `chain()` helper.
118+
* Object returned by `createCan`: typed boolean commands + a `chain()` helper.
119+
*
120+
* Includes AugmentedCanCommands so consumers who extend ExtensionCommandMap
121+
* via module augmentation get their custom commands on can() automatically.
84122
*/
85-
export type CanObject = KnownCanCommands &
86-
Record<string, CanCommand> & {
123+
export type CanObject = AllCommandSignatures &
124+
AugmentedCanCommands & {
87125
chain: () => ChainableCommandObject;
88126
};
89127

@@ -100,23 +138,11 @@ export type ExtensionCommands = Pick<KnownCommandRecord, keyof ExtensionCommandM
100138
/**
101139
* All available editor commands.
102140
*
103-
* Composed from explicit imports of each command interface for reliable
104-
* cross-package typing (module augmentation doesn't survive the npm boundary).
105-
* The Record<string, AnyCommand> fallback allows dynamic/plugin commands.
141+
* Composed from AllCommandSignatures (explicit imports) for reliable
142+
* cross-package typing, plus CoreCommands/ExtensionCommands (module
143+
* augmentation) and a Record fallback for dynamic/plugin commands.
106144
*/
107-
export type EditorCommands = CoreCommands &
108-
ExtensionCommands &
109-
CoreCommandSignatures &
110-
CommentCommands &
111-
FormattingCommandAugmentations &
112-
HistoryLinkTableCommandAugmentations &
113-
SpecializedCommandAugmentations &
114-
ParagraphCommands &
115-
BlockNodeCommands &
116-
ImageCommands &
117-
MiscellaneousCommands &
118-
TrackChangesCommands &
119-
Record<string, AnyCommand>;
145+
export type EditorCommands = CoreCommands & ExtensionCommands & AllCommandSignatures & Record<string, AnyCommand>;
120146

121147
/**
122148
* Command props made available to every command handler.

tests/consumer-typecheck/src/customer-scenario.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ function testEditorCommands(editor: Editor) {
270270

271271
// Chain API
272272
editor.chain().toggleBold().toggleItalic().run();
273+
274+
// SD-2334: Chain intermediate methods must return ChainableCommandObject, not boolean.
275+
// Reproduces IT-344 (Ontra): chain().setTextSelection(...).setMark(...).run()
276+
const chainResult: ChainableCommandObject = editor.chain().setTextSelection({ from: 0, to: 5 });
277+
const runResult: boolean = editor.chain().setTextSelection({ from: 0, to: 5 }).setMark('bold').run();
278+
279+
// SD-2334: can().chain() must return ChainableCommandObject, not boolean
280+
const canChain: ChainableCommandObject = editor.can().chain();
281+
const canChainRun: boolean = editor.can().chain().toggleBold().run();
273282
}
274283

275284
function testPresentationEditorCommands(pe: PresentationEditor) {

0 commit comments

Comments
 (0)