|
// ------------------------------------------------------------------------------------------------------------------------------------- |
|
// As a bonus, here's the UndoableChain adapted to the multiple OutputT style. |
|
// |
|
// Unfortunately, each Chain cannot know the ValueT of the Chain whose apply() produced it. |
|
// So undo() must produce a Chain<ValueT, ArrayHistory, UndoExtension<unknown>>, since we cannot know the type of the previous ValueT. |
|
// |
|
// Or rather, we can know, but we would have to add another generic type argument to UndoExtension and Chain called PrevValueT. |
|
// That would let undo() produce a Chain<ValueT, PrevValueT, ArrayHistory, UndoExtension<PrevValueT>>. |
|
// |
|
// But now consider the Chain<ValueT, PrevValueT, ArrayHistory, UndoExtension<PrevValueT>> that undo() produces. |
|
// If I try to undo() again, then the undo() must produce a Chain<PrevValueT, unknown, ArrayHistory, UndoExtension<unknown>> |
|
// |
|
// Thus, we must know the PrevPrevValueT in order to type-check undo() and allow you to undo() 2 times in a row. |
|
// Then, we'd have a Chain<PrevValueT, PrevPrevValueT, ArrayHistory, UndoExtension<PrevPrevValueT>> |
|
// |
|
// But now when we undo() 3 times in a row, we must know PrevPrevPrevValueT to typecheck the next undo(), and so on and so forth. |
|
// Therefore, we would need to know the amount of transformations and all the ValueTs at each step before this could be typecheckable. |
|
// |
|
// This is not possible, at least for my knowledge of Typescript, and defeats the purpose of typechecking in the first place. |
|
// It should enforce the rules and logic for you, not require you to manually provide all types ahead of time and worry about |
|
// compatibility then instead of as you write transformations. It makes the pattern much less modular. |
|
// ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
type ArrayHistory = { |
|
history: any[] |
|
} |
|
|
|
type Chain<ValueT, MetadataT, ExtensionT> = ExtensionT & { |
|
value: ValueT, |
|
apply: <OutputT>(transform: (value: ValueT) => OutputT) => Chain<OutputT, MetadataT, ExtensionT>, |
|
metadata: MetadataT |
|
} |
|
|
|
type UndoExtension<ValueT> = { |
|
undo: () => Chain<ValueT, ArrayHistory, UndoExtension<unknown>> |
|
} |
|
|
|
type UndoChain<T> = Chain<T, ArrayHistory, UndoExtension<unknown>>; |
|
|
|
function addValueToHistory<T>(value: T, metadata?: ArrayHistory): ArrayHistory { |
|
return metadata |
|
? { history: metadata.history.concat(value)} |
|
: { history: [value] } |
|
} |
|
|
|
function undoValueFromHistory<T>(metadata: ArrayHistory): ArrayHistory { |
|
return { |
|
history: metadata.history.slice(0, metadata.history.length - 2) |
|
} |
|
} |
|
|
|
function chainWrap<ValueT>(value: ValueT, transformMetadata: (value: unknown, metadata?: ArrayHistory) => ArrayHistory, metadata?: ArrayHistory): UndoChain<ValueT> { |
|
const nextMetadata: ArrayHistory = transformMetadata(value, metadata); |
|
return { |
|
value: value, |
|
apply: function apply<OutputT>(transform: (value: ValueT) => OutputT) { |
|
return chainWrap<OutputT>(transform(value), transformMetadata, nextMetadata); |
|
}, |
|
metadata: nextMetadata, |
|
undo: () => { |
|
const previousValue = nextMetadata.history[nextMetadata.history.length - 2]; |
|
return chainWrap(previousValue, addValueToHistory, undoValueFromHistory(nextMetadata)); |
|
} |
|
}; |
|
} |
|
|
|
function wrap<ValueT>(value: ValueT): UndoChain<ValueT> { |
|
return chainWrap(value, addValueToHistory); |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------------------------------------------- |
|
// And here are some usage examples: |
|
// ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
const result1: UndoChain<number> = |
|
wrap(2) |
|
.apply<number>(v => v + 1); |
|
|
|
console.log(result1.value); |
|
// 3 |
|
|
|
console.log(result1.metadata.history); |
|
// [2, 3] |
|
|
|
const result1a = result1.undo(); |
|
|
|
console.log(result1a.value); |
|
// 2 |
|
|
|
console.log(result1a.metadata.history); |
|
// [2] |
|
|
|
const result2: UndoChain<String> = |
|
wrap(2) |
|
.apply<number>((v) => v + 1) |
|
.apply<number>(v => v + 2) |
|
.apply<string>(v => "Test: " + v); |
|
|
|
console.log(result2.value); |
|
// "Test: 5" |
|
|
|
console.log(result2.metadata.history); |
|
// [2, 3, 5, "Test: 5"] |
|
|
|
const result2a = result2.undo(); |
|
|
|
console.log(result2a.value); |
|
// 5 |
|
|
|
console.log(result2a.metadata.history); |
|
// [2, 3, 5] |
|
|
|
const result2b = result2a.undo(); |
|
|
|
console.log(result2b.value); |
|
// 3 |
|
|
|
console.log(result2b.metadata.history); |
|
// [2, 3] |