typescript
56 lines · 7 steps
Undo/redo with two stacks in TypeScript
A three-part state model — past, present, future — powers bounded undo and redo for an editor.
Explained by
highlit
1type EditorState = {
2 content: string;
3 cursor: number;
4};
5
6export class UndoRedoHistory {
7 private past: EditorState[] = [];
8 private future: EditorState[] = [];
9 private present: EditorState;
10 private readonly limit: number;
11
12 constructor(initial: EditorState, limit = 100) {
13 this.present = initial;
14 this.limit = limit;
15 }
16
17 get current(): EditorState {
18 return this.present;
19 }
20
21 get canUndo(): boolean {
22 return this.past.length > 0;
23 }
24
25 get canRedo(): boolean {
26 return this.future.length > 0;
27 }
28
29 push(next: EditorState): void {
30 if (next.content === this.present.content && next.cursor === this.present.cursor) {
31 return;
32 }
33 this.past.push(this.present);
34 if (this.past.length > this.limit) {
35 this.past.shift();
36 }
37 this.present = next;
38 this.future = [];
39 }
40
41 undo(): EditorState {
42 const previous = this.past.pop();
43 if (!previous) return this.present;
44 this.future.unshift(this.present);
45 this.present = previous;
46 return this.present;
47 }
48
49 redo(): EditorState {
50 const next = this.future.shift();
51 if (!next) return this.present;
52 this.past.push(this.present);
53 this.present = next;
54 return this.present;
55 }
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Modeling history as past/present/future stacks makes undo and redo symmetric, mirror-image operations.
- 2Capping the past stack bounds memory so long editing sessions don't grow history without limit.
- 3Pushing a new state must clear the redo future, since diverging from history invalidates the old redo path.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/undo-redo-with-two-stacks-in-typescript-explained-typescript-26a0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.