Why This Matters
Strings in JavaScript are immutable. Every time you write str += chunk, the engine allocates a new string, copies all the existing characters, and appends the new ones. In a loop over n chunks, that is 1 + 2 + 3 + ... + n character copies — O(n²) total work.
A StringBuilder avoids this by keeping chunks in a mutable array buffer. Each append is an O(1) array push. The O(n) cost of assembling the final string is paid once, at the end, when toString() calls .join("").
Constraints
valuepassed toappendandprependis always a valid string (including empty string"")lengthmust be O(1), not computed on each calltoString()must return an empty string when the builder is empty- Mutating methods must return
thisfor chaining - Internal buffer must be private
Examples
const sb = new StringBuilder();
sb.append("Hello").append(", ").append("world");
sb.toString(); // "Hello, world"
sb.length; // 12
sb.prepend(">> ");
sb.toString(); // ">> Hello, world"
sb.length; // 15
sb.clear();
sb.toString(); // ""
sb.length; // 0Edge Cases Checklist
- Appending an empty string (
"") should not changelength toString()on a fresh builder returns""lengthis accurate after a mix ofappendandprependcallsclear()followed byappendworks correctly- Method chaining returns the same instance reference
- Prepending to an empty builder works the same as appending
Design Notes
Why length is tracked separately and not _buffer.length
_buffer.length counts the number of chunks in the array, not the number of characters. A single call to append("Hello, world") adds one element to the buffer but twelve characters to the string. Using _buffer.length as the length getter would return the chunk count, which is meaningless to callers.
Computing the real character count on demand would require summing chunk.length across every element — O(n) per call. Tracking it incrementally in append, prepend, and clear keeps the getter O(1).
The prepend tradeoff
prepend uses Array.unshift, which is O(n) because it shifts every existing element one position forward. If prepending in a tight loop is a requirement, a more suitable structure would maintain two arrays (one for front chunks, one for back) and reverse the front half during toString. For the scope of this implementation, prepend is included as a convenience with the O(n) cost documented.
Why join("") not concatenation in toString
join("") is a single allocation. It knows the total size of all chunks upfront and writes them into one contiguous buffer. A naive reduce with + inside toString would recreate the same O(n²) problem we are trying to avoid.
Approach
- Store a private
_buffer: string[]to hold chunks without allocating a new string on each operation. - Track
_length: numberseparately so thelengthgetter is always O(1). appendpushes to the end of the buffer and increments_length.prependunshifts to the front of the buffer and increments_length.clearreassigns both_bufferand_lengthto their zero states.toStringcallsthis._buffer.join("")— one allocation, one pass.- All mutating methods return
thisto enable chaining.
Complexity:
append: O(1) amortizedprepend: O(n) — shifts existing buffer elementsclear: O(1)length: O(1)toString: O(n)- Space: O(n) for the buffer
Implementation
export class StringBuilder {
private _buffer: string[] = [];
private _length: number = 0;
append(value: string): this {
this._buffer.push(value);
this._length += value.length;
return this;
}
prepend(value: string): this {
this._buffer.unshift(value);
this._length += value.length;
return this;
}
clear(): this {
this._buffer = [];
this._length = 0;
return this;
}
get length(): number {
return this._length;
}
toString(): string {
return this._buffer.join("");
}
}Test Coverage
The test suite covers:
- Empty builder returns
""andlengthof0 - Single
appendproduces correct string and length - Multiple chained
appendcalls produce correct result prependadds to the front correctly- Mixed
appendandprependcalls produce correct ordering - Appending empty string does not change
lengthor output clear()resets bothtoString()andlengthclear()followed by newappendworks correctly- Method chaining returns the same instance
lengthstays accurate through all operations- Large deterministic input check for correctness