0
# Transformations
1
2
The transformation system provides atomic document changes, position tracking, and mapping operations. It ensures consistency when multiple changes occur simultaneously and enables features like collaborative editing and undo/redo.
3
4
## Capabilities
5
6
### Transform Class
7
8
The main class for tracking and applying document changes.
9
10
```typescript { .api }
11
/**
12
* Tracks a series of steps that transform a document
13
*/
14
class Transform {
15
/**
16
* Create a new transform
17
*/
18
constructor(doc: Node);
19
20
/**
21
* The current document state
22
*/
23
doc: Node;
24
25
/**
26
* Array of steps applied to reach current state
27
*/
28
steps: Step[];
29
30
/**
31
* Array of documents after each step
32
*/
33
docs: Node[];
34
35
/**
36
* Current mapping from original positions
37
*/
38
mapping: Mapping;
39
40
/**
41
* Apply a step to the transform
42
*/
43
step(step: Step): Transform;
44
45
/**
46
* Apply a step if possible, return the transform
47
*/
48
maybeStep(step: Step): StepResult<Transform>;
49
}
50
```
51
52
### Step Class
53
54
Abstract base class for atomic document changes.
55
56
```typescript { .api }
57
/**
58
* Abstract base class for document transformation steps
59
*/
60
abstract class Step {
61
/**
62
* Apply this step to a document
63
*/
64
apply(doc: Node): StepResult<Node>;
65
66
/**
67
* Get the position mapping for this step
68
*/
69
getMap(): StepMap;
70
71
/**
72
* Create the inverse of this step
73
*/
74
invert(doc: Node): Step;
75
76
/**
77
* Map this step through a mapping
78
*/
79
map(mapping: Mappable): Step | null;
80
81
/**
82
* Try to merge this step with another step
83
*/
84
merge(other: Step): Step | null;
85
86
/**
87
* Convert step to JSON
88
*/
89
toJSON(): any;
90
91
/**
92
* Create step from JSON
93
*/
94
static fromJSON(schema: Schema, json: any): Step;
95
}
96
```
97
98
### Mapping Classes
99
100
Classes for tracking position changes through transformations.
101
102
```typescript { .api }
103
/**
104
* Maps positions through document changes
105
*/
106
class Mapping {
107
/**
108
* Create a new mapping
109
*/
110
constructor(maps?: StepMap[]);
111
112
/**
113
* Array of step maps
114
*/
115
maps: StepMap[];
116
117
/**
118
* Add a step map to the mapping
119
*/
120
appendMap(map: StepMap, mirrors?: number): void;
121
122
/**
123
* Append another mapping
124
*/
125
appendMapping(mapping: Mappable): void;
126
127
/**
128
* Map a position through the mapping
129
*/
130
map(pos: number, assoc?: number): number;
131
132
/**
133
* Map a position backwards
134
*/
135
mapResult(pos: number, assoc?: number): MapResult;
136
137
/**
138
* Get the slice of mapping from one index to another
139
*/
140
slice(from?: number, to?: number): Mapping;
141
}
142
143
/**
144
* Individual step's position mapping
145
*/
146
class StepMap {
147
/**
148
* Create a step map
149
*/
150
constructor(ranges: number[]);
151
152
/**
153
* Map a position through this step
154
*/
155
map(pos: number, assoc?: number): number;
156
157
/**
158
* Map a position with detailed result
159
*/
160
mapResult(pos: number, assoc?: number): MapResult;
161
162
/**
163
* Invert this step map
164
*/
165
invert(): StepMap;
166
167
/**
168
* Convert to string representation
169
*/
170
toString(): string;
171
172
/**
173
* Create empty step map
174
*/
175
static empty: StepMap;
176
177
/**
178
* Create offset step map
179
*/
180
static offset(n: number): StepMap;
181
}
182
```
183
184
### Built-in Step Types
185
186
Concrete step implementations for common operations.
187
188
```typescript { .api }
189
/**
190
* Step that replaces content between two positions
191
*/
192
class ReplaceStep extends Step {
193
/**
194
* Create a replace step
195
*/
196
constructor(from: number, to: number, slice: Slice, structure?: boolean);
197
198
/**
199
* From position
200
*/
201
from: number;
202
203
/**
204
* To position
205
*/
206
to: number;
207
208
/**
209
* Content to insert
210
*/
211
slice: Slice;
212
}
213
214
/**
215
* Step that replaces content around a position
216
*/
217
class ReplaceAroundStep extends Step {
218
/**
219
* Create a replace around step
220
*/
221
constructor(from: number, to: number, gapFrom: number, gapTo: number, slice: Slice, insert: number, structure?: boolean);
222
}
223
224
/**
225
* Step that adds or removes a mark
226
*/
227
class AddMarkStep extends Step {
228
/**
229
* Create an add mark step
230
*/
231
constructor(from: number, to: number, mark: Mark);
232
}
233
234
/**
235
* Step that removes a mark
236
*/
237
class RemoveMarkStep extends Step {
238
/**
239
* Create a remove mark step
240
*/
241
constructor(from: number, to: number, mark: Mark);
242
}
243
244
/**
245
* Step that adds node marks
246
*/
247
class AddNodeMarkStep extends Step {
248
/**
249
* Create an add node mark step
250
*/
251
constructor(pos: number, mark: Mark);
252
}
253
254
/**
255
* Step that removes node marks
256
*/
257
class RemoveNodeMarkStep extends Step {
258
/**
259
* Create a remove node mark step
260
*/
261
constructor(pos: number, mark: Mark);
262
}
263
```
264
265
### Transform Helper Functions
266
267
Utility functions for common transformation operations.
268
269
```typescript { .api }
270
/**
271
* Replace content between positions
272
*/
273
function replaceStep(doc: Node, from: number, to?: number, slice?: Slice): Step | null;
274
275
/**
276
* Lift content out of its parent
277
*/
278
function liftTarget(range: NodeRange): number | null;
279
280
/**
281
* Find wrapping for content
282
*/
283
function findWrapping(range: NodeRange, nodeType: NodeType, attrs?: Attrs, innerRange?: NodeRange): Transform | null;
284
285
/**
286
* Check if content can be split
287
*/
288
function canSplit(doc: Node, pos: number, depth?: number, typesAfter?: NodeType[]): boolean;
289
290
/**
291
* Split content at position
292
*/
293
function split(tr: Transform, pos: number, depth?: number, typesAfter?: NodeType[]): Transform;
294
295
/**
296
* Check if content can be joined
297
*/
298
function canJoin(doc: Node, pos: number): boolean;
299
300
/**
301
* Join content at position
302
*/
303
function joinPoint(doc: Node, pos: number, dir?: number): number | null;
304
305
/**
306
* Insert content at position
307
*/
308
function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null;
309
310
/**
311
* Drop point for content
312
*/
313
function dropPoint(doc: Node, pos: number, slice: Slice): number | null;
314
```
315
316
**Usage Examples:**
317
318
```typescript
319
import {
320
Transform,
321
Step,
322
ReplaceStep,
323
AddMarkStep,
324
RemoveMarkStep,
325
Mapping,
326
StepMap,
327
replaceStep,
328
canSplit,
329
canJoin
330
} from "@tiptap/pm/transform";
331
332
// Basic document transformation
333
function performTransformation(doc: Node, schema: Schema): Node {
334
const tr = new Transform(doc);
335
336
// Insert text at position 10
337
const insertStep = new ReplaceStep(
338
10, 10,
339
new Slice(Fragment.from(schema.text("Hello, ")), 0, 0)
340
);
341
tr.step(insertStep);
342
343
// Add bold mark to text from position 10-16
344
const boldMark = schema.marks.strong.create();
345
const markStep = new AddMarkStep(10, 16, boldMark);
346
tr.step(markStep);
347
348
// Replace content at position 20-25
349
const replaceSlice = new Slice(
350
Fragment.from(schema.text("World!")),
351
0, 0
352
);
353
const replaceStep = new ReplaceStep(20, 25, replaceSlice);
354
tr.step(replaceStep);
355
356
return tr.doc;
357
}
358
359
// Position mapping through transformations
360
function trackPositionThroughChanges(
361
doc: Node,
362
originalPos: number,
363
steps: Step[]
364
): number {
365
const mapping = new Mapping();
366
let currentDoc = doc;
367
368
for (const step of steps) {
369
const result = step.apply(currentDoc);
370
if (result.failed) {
371
throw new Error(`Step failed: ${result.failed}`);
372
}
373
374
currentDoc = result.doc;
375
mapping.appendMap(step.getMap());
376
}
377
378
return mapping.map(originalPos);
379
}
380
381
// Complex transformation with validation
382
class DocumentEditor {
383
private doc: Node;
384
private schema: Schema;
385
386
constructor(doc: Node, schema: Schema) {
387
this.doc = doc;
388
this.schema = schema;
389
}
390
391
insertText(pos: number, text: string): boolean {
392
const tr = new Transform(this.doc);
393
const slice = new Slice(
394
Fragment.from(this.schema.text(text)),
395
0, 0
396
);
397
398
const step = new ReplaceStep(pos, pos, slice);
399
const result = tr.maybeStep(step);
400
401
if (result.failed) {
402
console.error("Insert failed:", result.failed);
403
return false;
404
}
405
406
this.doc = tr.doc;
407
return true;
408
}
409
410
deleteRange(from: number, to: number): boolean {
411
const tr = new Transform(this.doc);
412
const step = new ReplaceStep(from, to, Slice.empty);
413
const result = tr.maybeStep(step);
414
415
if (result.failed) {
416
console.error("Delete failed:", result.failed);
417
return false;
418
}
419
420
this.doc = tr.doc;
421
return true;
422
}
423
424
toggleMark(from: number, to: number, markType: MarkType, attrs?: Attrs): boolean {
425
const tr = new Transform(this.doc);
426
const mark = markType.create(attrs);
427
428
// Check if mark exists in range
429
const hasMark = this.doc.rangeHasMark(from, to, markType);
430
431
let step: Step;
432
if (hasMark) {
433
// Remove mark
434
step = new RemoveMarkStep(from, to, mark);
435
} else {
436
// Add mark
437
step = new AddMarkStep(from, to, mark);
438
}
439
440
const result = tr.maybeStep(step);
441
if (result.failed) {
442
console.error("Toggle mark failed:", result.failed);
443
return false;
444
}
445
446
this.doc = tr.doc;
447
return true;
448
}
449
450
splitBlock(pos: number): boolean {
451
if (!canSplit(this.doc, pos)) {
452
return false;
453
}
454
455
const tr = new Transform(this.doc);
456
const $pos = this.doc.resolve(pos);
457
const nodeType = $pos.parent.type;
458
459
const splitStep = new ReplaceStep(
460
pos, pos,
461
new Slice(
462
Fragment.from([
463
nodeType.createAndFill(),
464
nodeType.createAndFill()
465
]),
466
1, 1
467
)
468
);
469
470
const result = tr.maybeStep(splitStep);
471
if (result.failed) {
472
console.error("Split failed:", result.failed);
473
return false;
474
}
475
476
this.doc = tr.doc;
477
return true;
478
}
479
480
joinBlocks(pos: number): boolean {
481
if (!canJoin(this.doc, pos)) {
482
return false;
483
}
484
485
const tr = new Transform(this.doc);
486
const $pos = this.doc.resolve(pos);
487
const before = $pos.nodeBefore;
488
const after = $pos.nodeAfter;
489
490
if (!before || !after) return false;
491
492
const joinStep = new ReplaceStep(
493
pos - before.nodeSize,
494
pos + after.nodeSize,
495
new Slice(
496
Fragment.from(before.content.append(after.content)),
497
0, 0
498
)
499
);
500
501
const result = tr.maybeStep(joinStep);
502
if (result.failed) {
503
console.error("Join failed:", result.failed);
504
return false;
505
}
506
507
this.doc = tr.doc;
508
return true;
509
}
510
511
getDocument(): Node {
512
return this.doc;
513
}
514
}
515
```
516
517
## Advanced Transformation Features
518
519
### Custom Step Types
520
521
Create specialized step types for specific operations.
522
523
```typescript
524
/**
525
* Custom step for atomic table operations
526
*/
527
class TableTransformStep extends Step {
528
constructor(
529
private tablePos: number,
530
private operation: "addColumn" | "addRow" | "deleteColumn" | "deleteRow",
531
private index: number
532
) {
533
super();
534
}
535
536
apply(doc: Node): StepResult<Node> {
537
try {
538
const $pos = doc.resolve(this.tablePos);
539
const table = $pos.nodeAfter;
540
541
if (!table || table.type.name !== "table") {
542
return StepResult.fail("No table found at position");
543
}
544
545
let newTable: Node;
546
547
switch (this.operation) {
548
case "addColumn":
549
newTable = this.addColumn(table);
550
break;
551
case "addRow":
552
newTable = this.addRow(table);
553
break;
554
case "deleteColumn":
555
newTable = this.deleteColumn(table);
556
break;
557
case "deleteRow":
558
newTable = this.deleteRow(table);
559
break;
560
default:
561
return StepResult.fail("Unknown table operation");
562
}
563
564
const newDoc = doc.copy(
565
doc.content.replaceChild(
566
this.tablePos,
567
newTable
568
)
569
);
570
571
return StepResult.ok(newDoc);
572
} catch (error) {
573
return StepResult.fail(error.message);
574
}
575
}
576
577
getMap(): StepMap {
578
// Calculate position changes based on operation
579
switch (this.operation) {
580
case "addColumn":
581
case "addRow":
582
return new StepMap([
583
this.tablePos, 0, 1 // Insert at table position
584
]);
585
case "deleteColumn":
586
case "deleteRow":
587
return new StepMap([
588
this.tablePos, 1, 0 // Delete at table position
589
]);
590
default:
591
return StepMap.empty;
592
}
593
}
594
595
invert(doc: Node): Step {
596
// Return inverse operation
597
const inverseOps = {
598
"addColumn": "deleteColumn",
599
"addRow": "deleteRow",
600
"deleteColumn": "addColumn",
601
"deleteRow": "addRow"
602
};
603
604
return new TableTransformStep(
605
this.tablePos,
606
inverseOps[this.operation] as any,
607
this.index
608
);
609
}
610
611
map(mapping: Mappable): Step | null {
612
const newPos = mapping.map(this.tablePos);
613
return new TableTransformStep(newPos, this.operation, this.index);
614
}
615
616
merge(other: Step): Step | null {
617
// Table steps don't merge with other operations
618
return null;
619
}
620
621
toJSON(): any {
622
return {
623
stepType: "tableTransform",
624
tablePos: this.tablePos,
625
operation: this.operation,
626
index: this.index
627
};
628
}
629
630
static fromJSON(schema: Schema, json: any): TableTransformStep {
631
return new TableTransformStep(
632
json.tablePos,
633
json.operation,
634
json.index
635
);
636
}
637
638
private addColumn(table: Node): Node {
639
// Implementation for adding column
640
return table; // Simplified
641
}
642
643
private addRow(table: Node): Node {
644
// Implementation for adding row
645
return table; // Simplified
646
}
647
648
private deleteColumn(table: Node): Node {
649
// Implementation for deleting column
650
return table; // Simplified
651
}
652
653
private deleteRow(table: Node): Node {
654
// Implementation for deleting row
655
return table; // Simplified
656
}
657
}
658
```
659
660
### Operational Transform
661
662
Handle conflicting simultaneous transformations.
663
664
```typescript
665
class OperationalTransform {
666
/**
667
* Transform two concurrent operations for conflict resolution
668
*/
669
static transform(
670
stepA: Step,
671
stepB: Step,
672
priority: "left" | "right" = "left"
673
): { stepA: Step | null; stepB: Step | null } {
674
// Handle different step type combinations
675
if (stepA instanceof ReplaceStep && stepB instanceof ReplaceStep) {
676
return this.transformReplaceSteps(stepA, stepB, priority);
677
}
678
679
if (stepA instanceof AddMarkStep && stepB instanceof AddMarkStep) {
680
return this.transformMarkSteps(stepA, stepB, priority);
681
}
682
683
// Handle mixed types
684
if (stepA instanceof ReplaceStep && stepB instanceof AddMarkStep) {
685
return this.transformReplaceAndMark(stepA, stepB);
686
}
687
688
if (stepA instanceof AddMarkStep && stepB instanceof ReplaceStep) {
689
const result = this.transformReplaceAndMark(stepB, stepA);
690
return { stepA: result.stepB, stepB: result.stepA };
691
}
692
693
// Default: apply mapping
694
const mapA = stepA.getMap();
695
const mapB = stepB.getMap();
696
697
return {
698
stepA: stepA.map(new Mapping([mapB])),
699
stepB: stepB.map(new Mapping([mapA]))
700
};
701
}
702
703
private static transformReplaceSteps(
704
stepA: ReplaceStep,
705
stepB: ReplaceStep,
706
priority: "left" | "right"
707
): { stepA: Step | null; stepB: Step | null } {
708
const aFrom = stepA.from;
709
const aTo = stepA.to;
710
const bFrom = stepB.from;
711
const bTo = stepB.to;
712
713
// No overlap - simple mapping
714
if (aTo <= bFrom) {
715
const offset = stepA.slice.size - (aTo - aFrom);
716
return {
717
stepA: stepA,
718
stepB: new ReplaceStep(bFrom + offset, bTo + offset, stepB.slice)
719
};
720
}
721
722
if (bTo <= aFrom) {
723
const offset = stepB.slice.size - (bTo - bFrom);
724
return {
725
stepA: new ReplaceStep(aFrom + offset, aTo + offset, stepA.slice),
726
stepB: stepB
727
};
728
}
729
730
// Overlapping changes - use priority
731
if (priority === "left") {
732
return {
733
stepA: stepA,
734
stepB: null // Discard conflicting step
735
};
736
} else {
737
return {
738
stepA: null,
739
stepB: stepB
740
};
741
}
742
}
743
744
private static transformMarkSteps(
745
stepA: AddMarkStep,
746
stepB: AddMarkStep,
747
priority: "left" | "right"
748
): { stepA: Step | null; stepB: Step | null } {
749
// Mark steps can usually coexist
750
if (stepA.mark.type !== stepB.mark.type) {
751
return { stepA, stepB };
752
}
753
754
// Same mark type - check for conflicts
755
const aFrom = stepA.from;
756
const aTo = stepA.to;
757
const bFrom = stepB.from;
758
const bTo = stepB.to;
759
760
// No overlap
761
if (aTo <= bFrom || bTo <= aFrom) {
762
return { stepA, stepB };
763
}
764
765
// Overlapping same mark - merge or prioritize
766
if (stepA.mark.eq(stepB.mark)) {
767
// Same mark - create merged step
768
const mergedStep = new AddMarkStep(
769
Math.min(aFrom, bFrom),
770
Math.max(aTo, bTo),
771
stepA.mark
772
);
773
return { stepA: mergedStep, stepB: null };
774
}
775
776
// Different attributes - use priority
777
return priority === "left"
778
? { stepA, stepB: null }
779
: { stepA: null, stepB };
780
}
781
782
private static transformReplaceAndMark(
783
replaceStep: ReplaceStep,
784
markStep: AddMarkStep
785
): { stepA: Step | null; stepB: Step | null } {
786
const replaceFrom = replaceStep.from;
787
const replaceTo = replaceStep.to;
788
const markFrom = markStep.from;
789
const markTo = markStep.to;
790
791
// Mark is completely before replace
792
if (markTo <= replaceFrom) {
793
return { stepA: replaceStep, stepB: markStep };
794
}
795
796
// Mark is completely after replace
797
if (markFrom >= replaceTo) {
798
const offset = replaceStep.slice.size - (replaceTo - replaceFrom);
799
return {
800
stepA: replaceStep,
801
stepB: new AddMarkStep(
802
markFrom + offset,
803
markTo + offset,
804
markStep.mark
805
)
806
};
807
}
808
809
// Mark overlaps with replace - complex transformation needed
810
// Simplified: apply mark to replacement content if applicable
811
return { stepA: replaceStep, stepB: null };
812
}
813
}
814
```
815
816
### Transform Validation
817
818
Validate transformations before applying them.
819
820
```typescript
821
class TransformValidator {
822
static validate(transform: Transform, schema: Schema): ValidationResult {
823
const errors: string[] = [];
824
const warnings: string[] = [];
825
826
// Validate each step
827
for (let i = 0; i < transform.steps.length; i++) {
828
const step = transform.steps[i];
829
const doc = i === 0 ? transform.docs[0] : transform.docs[i];
830
831
const stepResult = this.validateStep(step, doc, schema);
832
errors.push(...stepResult.errors);
833
warnings.push(...stepResult.warnings);
834
}
835
836
// Validate final document
837
const finalValidation = this.validateDocument(transform.doc, schema);
838
errors.push(...finalValidation.errors);
839
warnings.push(...finalValidation.warnings);
840
841
return {
842
valid: errors.length === 0,
843
errors,
844
warnings
845
};
846
}
847
848
private static validateStep(step: Step, doc: Node, schema: Schema): ValidationResult {
849
const errors: string[] = [];
850
const warnings: string[] = [];
851
852
try {
853
// Test applying the step
854
const result = step.apply(doc);
855
if (result.failed) {
856
errors.push(`Step application failed: ${result.failed}`);
857
}
858
859
// Validate step-specific constraints
860
if (step instanceof ReplaceStep) {
861
const validation = this.validateReplaceStep(step, doc, schema);
862
errors.push(...validation.errors);
863
warnings.push(...validation.warnings);
864
}
865
866
if (step instanceof AddMarkStep) {
867
const validation = this.validateMarkStep(step, doc, schema);
868
errors.push(...validation.errors);
869
warnings.push(...validation.warnings);
870
}
871
872
} catch (error) {
873
errors.push(`Step validation error: ${error.message}`);
874
}
875
876
return { valid: errors.length === 0, errors, warnings };
877
}
878
879
private static validateReplaceStep(
880
step: ReplaceStep,
881
doc: Node,
882
schema: Schema
883
): ValidationResult {
884
const errors: string[] = [];
885
const warnings: string[] = [];
886
887
// Check position bounds
888
if (step.from < 0 || step.to > doc.content.size) {
889
errors.push(`Replace step positions out of bounds: ${step.from}-${step.to}`);
890
}
891
892
if (step.from > step.to) {
893
errors.push(`Invalid replace range: from(${step.from}) > to(${step.to})`);
894
}
895
896
// Check content compatibility
897
try {
898
const $from = doc.resolve(step.from);
899
const $to = doc.resolve(step.to);
900
901
if (!$from.parent.canReplace($from.index(), $to.index(), step.slice.content)) {
902
errors.push("Replacement content not allowed at position");
903
}
904
} catch (error) {
905
errors.push(`Position resolution failed: ${error.message}`);
906
}
907
908
return { valid: errors.length === 0, errors, warnings };
909
}
910
911
private static validateMarkStep(
912
step: AddMarkStep,
913
doc: Node,
914
schema: Schema
915
): ValidationResult {
916
const errors: string[] = [];
917
const warnings: string[] = [];
918
919
// Check if mark can be applied to content in range
920
try {
921
doc.nodesBetween(step.from, step.to, (node, pos) => {
922
if (node.isText && !node.type.allowsMarkType(step.mark.type)) {
923
errors.push(`Mark ${step.mark.type.name} not allowed on text at ${pos}`);
924
}
925
return true;
926
});
927
} catch (error) {
928
errors.push(`Mark validation failed: ${error.message}`);
929
}
930
931
return { valid: errors.length === 0, errors, warnings };
932
}
933
934
private static validateDocument(doc: Node, schema: Schema): ValidationResult {
935
const errors: string[] = [];
936
const warnings: string[] = [];
937
938
try {
939
// Use ProseMirror's built-in validation
940
doc.check();
941
} catch (error) {
942
errors.push(`Document structure invalid: ${error.message}`);
943
}
944
945
return { valid: errors.length === 0, errors, warnings };
946
}
947
}
948
949
interface ValidationResult {
950
valid: boolean;
951
errors: string[];
952
warnings: string[];
953
}
954
```
955
956
## Types
957
958
```typescript { .api }
959
/**
960
* Result of applying a step
961
*/
962
interface StepResult<T> {
963
/**
964
* The resulting document/transform if successful
965
*/
966
doc?: T;
967
968
/**
969
* Error message if failed
970
*/
971
failed?: string;
972
}
973
974
/**
975
* Position mapping result with additional information
976
*/
977
interface MapResult {
978
/**
979
* Mapped position
980
*/
981
pos: number;
982
983
/**
984
* Whether position was deleted
985
*/
986
deleted: boolean;
987
988
/**
989
* Recovery information
990
*/
991
recover?: number;
992
}
993
994
/**
995
* Interface for mappable objects
996
*/
997
interface Mappable {
998
/**
999
* Map a position
1000
*/
1001
map(pos: number, assoc?: number): number;
1002
1003
/**
1004
* Map with detailed result
1005
*/
1006
mapResult(pos: number, assoc?: number): MapResult;
1007
}
1008
1009
/**
1010
* Step result helper
1011
*/
1012
class StepResult<T> {
1013
static ok<T>(value: T): StepResult<T>;
1014
static fail<T>(message: string): StepResult<T>;
1015
}
1016
```