Left-Pad Refactoring Lecture
A live walkthrough of the famous npm left-pad module — starting from the original 2016 source and a Jest-style test suite, then replacing the whole module with one ES2017 built-in (`String.prototype.padStart`) and watching three tests break.
The Module That Broke the Internet
Why this matters
In March 2016, a developer un-published an eleven-line npm package called left-pad. Within hours, Babel, React, and thousands of other projects stopped building — npm install couldn’t resolve the transitive dependency. The incident kicked off years of debate about supply-chain risk, micro-dependencies, and what belongs in a standard library. This lecture replays the story by reading the actual code, running its tests, refactoring it to modern JavaScript, and finally replacing the whole module with one line of String.prototype.padStart.
🎯 You will learn to
- Read a tiny but production-shipped JavaScript module and identify its contract
- Apply a Jest-style test suite (
describe/test/expect) as the contract - Refactor a 2016-era
while-loop implementation to a 2017-eraString.prototype.repeatone - Evaluate what behaviors a test suite actually protects vs. assumes
The original leftpad
The pre-incident left-pad package was famously short — eleven lines. The version below is the canonical one from before the optimisation patches: var i = -1, a manual while (++i < length) loop, and a hand-rolled default for the pad character. It works, but every line is doing something modern JavaScript has a built-in for.
function leftpad (str, length, paddingCharacter) {
str = String(str);
var i = -1;
if (!paddingCharacter && paddingCharacter !== 0) paddingCharacter = ' ';
length = length - str.length;
while (++i < length) {
str = paddingCharacter + str;
}
return str;
}
| Line | What it does |
|---|---|
str = String(str) |
Coerce non-string input (numbers, null, undefined) into a string — this matters in step 2 |
if (!paddingCharacter && paddingCharacter !== 0) ch = ' ' |
Hand-rolled default: space, but 0 is allowed too |
length = length - str.length |
Reuse length as the number of pad characters still to prepend |
while (++i < length) str = paddingCharacter + str |
Prepend one character per iteration — allocates a new string each pass |
The driver: main.js
main.js is the small program above that exercises leftpad. By default it pads four numbers (124, 1, 90093, 9193) to width 5. If you pass command-line arguments via the argv field above the Run button, those numbers replace the defaults — try 7 42 1024 and watch the output line up.
The contract — as Jest-style tests
__tests__/leftpad_test.js encodes the contract in nine Jest-style expect(...).toBe(...) assertions. A tiny in-file runner at the top of the file gives us describe / test / expect without an npm install jest, so node __tests__/leftpad_test.js works as-is — the assertions are exactly what real Jest would evaluate.
Two buttons run two different things:
- ▶ Run —
node main.js <argv>. The number you type into the argv field is appended verbatim. The output is one padded line per number. - ✓ Test —
node __tests__/leftpad_test.js. The output is a✓ / ✗line per test plus a summary count.
✏️ Predict before you run
The starter left-pad.js is the 2016 original. Three modern JavaScript features can replace three of its lines without changing the contract:
paddingCharacter.repeat(n)returnspaddingCharacterconcatenatedntimes — one call replaces thewhile (++i < length)loop.- Default parameters (
paddingCharacter = ' ') replace the hand-rolledif (!paddingCharacter ...)default. - An early-return guard (
if (length < 1) return str) replaces the no-op behavior thewhileloop got “for free” whenlength <= 0.
Which of those three changes can you make without breaking any of the nine Jest tests?
- (a) Only the loop replacement.
- (b) Only the default-parameter change.
- (c) All three — the tests describe the contract, not the implementation.
- (d) None — refactoring will break the numeric / null / undefined tests.
Commit to a letter, then click ▶ Run and ✓ Test.
Reveal (after committing)
**(c)**. The nine tests describe the *contract* — what `leftpad` returns for given inputs. They don't care how it's implemented. Any refactor that preserves the contract (correct default, no truncation, character repetition, `String()` coercion) will keep them green. (d) is the trap from step 2, not this one: the *built-in* `padStart` doesn't coerce, so dropping `String(str)` breaks 3 tests. But our refactor keeps `String(str)`.Task
- Read
left-pad.js(left pane) and__tests__/leftpad_test.js(right pane). - Click ▶ Run to execute
node main.js 124 1 90093 9193— confirm it prints four right-aligned numbers. - Click ✓ Test to execute the Jest suite. Confirm all nine tests pass against the 2016 original.
- Now refactor
leftpadto the modern eight-line shape:- Replace the
while (++i < length)loop with one call topaddingCharacter.repeat(length). - Replace the hand-rolled default with a default parameter
paddingCharacter = ' '. - Add an explicit
if (length < 1) return strguard solength <= 0cases still return early.
- Replace the
- Click ✓ Test again. All nine tests should still pass.
- (Instructor mode) Click Solution to compare against the reference implementation.
// The original left-pad — the eleven-line module that broke the
// internet in March 2016. Reproduced here verbatim from the
// pre-incident package, lightly renamed (str/length/paddingCharacter
// instead of str/len/ch) to match the rest of the lecture.
//
// 🛠️ TASK: Refactor this to the modern eight-line shape (see the
// instructions panel). The nine Jest tests on the right MUST stay
// green throughout — they describe the contract, not the body.
module.exports = leftpad;
function leftpad (str, length, paddingCharacter) {
str = String(str);
var i = -1;
if (!paddingCharacter && paddingCharacter !== 0) paddingCharacter = ' ';
length = length - str.length;
while (++i < length) {
str = paddingCharacter + str;
}
return str;
}
// main.js — a small driver that pads numbers with spaces.
//
// node main.js # default: 124 1 90093 9193
// node main.js 7 42 1024 # use these numbers instead
//
// The argv field above the Run button feeds into process.argv.
const leftpad = require('./left-pad.js');
const argv = process.argv.slice(2);
const numbers = argv.length > 0 ? argv : [124, 1, 90093, 9193];
const len = 5;
for (const num of numbers) {
console.log(leftpad(num, len, ' '));
}
// Jest-style contract tests for the leftpad module.
//
// Run with: node __tests__/leftpad_test.js
//
// In a real project you'd `npm install jest && npx jest`. The
// 15-line shim at the top of this file gives us the same
// describe / test / expect API as a plain Node script, so the
// demo doesn't need to install Jest.
// === Minimal Jest-compatible runner ====================================
let __passed = 0, __failed = 0;
function describe(name, fn) { console.log(name); fn(); }
function test(name, fn) {
try { fn(); console.log(' ✓ ' + name); __passed++; }
catch (e) { console.log(' ✗ ' + name); console.log(' ' + e.message); __failed++; }
}
function expect(actual) {
return { toBe(expected) {
if (!Object.is(actual, expected)) {
throw new Error('Expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual));
}
}};
}
// ========================================================================
const leftpad = require('../left-pad.js');
describe('leftpad', () => {
test('should pad a short string with spaces to the specified length', () => {
expect(leftpad('foo', 5)).toBe(' foo');
});
test('should return the original string if its length is equal to the target length', () => {
expect(leftpad('foobar', 6)).toBe('foobar');
});
test('should return the original string if its length is greater than the target length', () => {
expect(leftpad('foobar', 3)).toBe('foobar');
});
test('should pad with a custom character', () => {
expect(leftpad('foo', 5, '0')).toBe('00foo');
});
test('should return the original string when target length is 0', () => {
expect(leftpad('foo', 0)).toBe('foo');
});
test('should handle numeric input by converting it to a string', () => {
expect(leftpad(123, 5, '0')).toBe('00123');
});
test('should handle null input by converting it to the string "null"', () => {
expect(leftpad(null, 6)).toBe(' null');
});
test('should handle undefined input by converting it to the string "undefined"', () => {
expect(leftpad(undefined, 12)).toBe(' undefined');
});
test('should pad an empty string', () => {
expect(leftpad('', 4, 'x')).toBe('xxxx');
});
});
console.log('\n' + __passed + ' passed, ' + __failed + ' failed');
process.exit(__failed > 0 ? 1 : 0);
{
"name": "left-pad-lecture",
"version": "1.0.0",
"scripts": {
"test": "jest",
"dev": "node main.js"
},
"devDependencies": {
"jest": "^30.2.0"
}
}
Solution
// Modernised left-pad — same eleven-line contract, eight lines of
// ES2017. Default parameter replaces the hand-rolled `if (!ch ...)`.
// `paddingCharacter.repeat(length)` replaces the `while (++i < length)`
// loop. The explicit `if (length < 1) return str` makes the
// no-truncation guarantee a contract, not an emergent property of
// the loop.
module.exports = leftpad;
function leftpad (str, length, paddingCharacter = ' ') {
str = String(str);
length = length - str.length;
if (length < 1) {
return str;
}
str = paddingCharacter.repeat(length) + str;
return str;
}
What changed:
-
paddingCharacter = ' '— a default parameter (ES2015). The 2016 original usedif (!paddingCharacter && paddingCharacter !== 0) paddingCharacter = ' 'to allow the literal0as a valid pad character while still defaulting to a space. The default-parameter form has the same effect (it only kicks in when the argument isundefined, not when it’s falsy) and reads as the intent: “if the caller omits the pad, use a space.” -
paddingCharacter.repeat(length)—String.prototype.repeat(ES2015). One V8-implemented call replaces thevar i = -1; while (++i < length) str = paddingCharacter + strloop. Same result, no intermediate string allocations per iteration. -
if (length < 1) return str— explicit no-truncation guard. In the original, when the input was already long enough,length - str.lengthwas zero or negative and thewhilecondition++i < lengthwas immediately false, so the loop never ran. The early return makes that “implicit no-op” explicit — and matters in step 2 when we look atpadStart, which has the same rule baked into the spec.
What stayed: str = String(str). That’s the coercion every implementation in this lecture needs to keep — the nine tests include leftpad(123, 5, '0'), leftpad(null, 6), and leftpad(undefined, 12). Drop the String(str) line and three tests break; we’ll see exactly that in step 2.
Step 1 — Knowledge Check
Min. score: 80%
1. The original leftpad starts with str = String(str). If you removed that line and called leftpad(7, 4, '0'), what would happen?
(7).length is undefined, so length - str.length is NaN. NaN < 1 is false, so the early-return check doesn’t fire, and paddingCharacter.repeat(NaN) throws RangeError: Invalid count value. The thrown error is specifically a RangeError from repeat(NaN). The explicit str = String(str) line is what makes the numeric / null / undefined tests pass.
2. The test suite calls expect(leftpad('foobar', 3)).toBe('foobar'). What contract does this test encode?
The contract is one-sided: pad only when the input is shorter than the target length; otherwise return the input as-is. That’s why 'foobar'.padStart(3) behaves identically — padStart follows the same “no-truncation” rule, which is part of why it’s a valid replacement candidate.
One Built-In Replaces Eleven Lines: `String.prototype.padStart`
Why this matters
The reason left-pad no longer needs to exist is ES2017 (a.k.a. ES8). That language revision added String.prototype.padStart(targetLength, padString) and its sibling padEnd to the standard library. Node.js 8+ ships them. Two million npm dependents could replace the whole module with one method call — if they’re willing to accept the spec-correct semantics that come with it.
🎯 You will learn to
- Apply
String.prototype.padStartto replace the body ofleftpad - Analyze which behaviors are preserved and which break under the swap
- Evaluate when “spec-correct” differs from “what the previous implementation actually did”
String.prototype.padStart
'foo'.padStart(5) // ' foo'
'foo'.padStart(5, '0') // '00foo'
'foobar'.padStart(3) // 'foobar' ← no truncation, matches leftpad
Three reasons this is “the most efficient Node.js call that replaces left-pad”:
- Zero dependencies — it’s part of the language runtime.
- Zero bytes shipped — no
npm install, norequire. - Implemented in V8 C++ — orders of magnitude faster than allocating new strings in JS.
The naive swap
The temptation is to drop the body of leftpad and forward directly to the built-in:
function leftpad (str, length, paddingCharacter) {
return str.padStart(length, paddingCharacter);
}
Two lines. The whole module.
✏️ Predict before you run
How many of the nine tests still pass?
- (a) All nine —
padStartis the standard library version of exactly this function. - (b) Six — three tests rely on a JS-specific coercion the built-in doesn’t replicate.
- (c) Five —
padStarthas different default behavior for the pad character. - (d) Zero —
padStarttruncates instead of preserving short strings.
Commit to a letter, then click ▶ Run (try node main.js 124 1 90093 9193) and ✓ Test My Work. Read the test output carefully.
Reveal (after running)
**(b)**. Six of the nine pass. The three that break are the non-string-input tests: `leftpad(123, 5, '0')`, `leftpad(null, 6)`, and `leftpad(undefined, 12)`. Each throws `TypeError: str.padStart is not a function` (or similar) because `(123).padStart`, `null.padStart`, and `undefined.padStart` are not defined — only the *string* prototype has `padStart`. The original module masked all three with its first line, `str = String(str)`. The naive swap dropped that line — so the contract for non-string input is gone. Note: when you click **▶ Run**, you'll also see `main.js` crash with the same `TypeError`, because the default argv contains numbers. Try passing strings instead (`node main.js abc def ghi` via the argv field) and watch Run succeed even though the suite still fails.Task
- Replace the body of
leftpadinleft-pad.jswithreturn str.padStart(length, paddingCharacter); - Click ▶ Run. The default argv (numbers) will crash —
(124).padStartis not a function. - Now type
abc def ghiinto the argv field and click ▶ Run again. The string argv works. - Click ✓ Test My Work. Six tests should pass, three should fail (numeric / null / undefined).
- Read the failing tests’ names and error messages. Confirm they’re the non-string-input cases.
- Step 3 will fix it — but first, understand the break.
// The original left-pad — soon to be replaced with one line.
//
// 🛠️ TASK: Replace the body of leftpad with:
//
// return str.padStart(length, paddingCharacter);
//
// Then click ▶ Run (notice non-string argv crashes) and ✓ Test My Work
// (6 tests pass, 3 fail).
module.exports = leftpad;
function leftpad (str, length, paddingCharacter = ' ') {
str = String(str);
length = length - str.length;
if (length < 1) {
return str;
}
str = paddingCharacter.repeat(length) + str;
return str;
}
// main.js — same driver as step 1.
const leftpad = require('./left-pad.js');
const argv = process.argv.slice(2);
const numbers = argv.length > 0 ? argv : [124, 1, 90093, 9193];
const len = 5;
for (const num of numbers) {
console.log(leftpad(num, len, ' '));
}
// Same Jest-style contract tests as step 1 — unchanged.
// The point is to watch the test suite respond to a change in
// left-pad.js.
let __passed = 0, __failed = 0;
function describe(name, fn) { console.log(name); fn(); }
function test(name, fn) {
try { fn(); console.log(' ✓ ' + name); __passed++; }
catch (e) { console.log(' ✗ ' + name); console.log(' ' + e.message); __failed++; }
}
function expect(actual) {
return { toBe(expected) {
if (!Object.is(actual, expected)) {
throw new Error('Expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual));
}
}};
}
const leftpad = require('../left-pad.js');
describe('leftpad', () => {
test('should pad a short string with spaces to the specified length', () => {
expect(leftpad('foo', 5)).toBe(' foo');
});
test('should return the original string if its length is equal to the target length', () => {
expect(leftpad('foobar', 6)).toBe('foobar');
});
test('should return the original string if its length is greater than the target length', () => {
expect(leftpad('foobar', 3)).toBe('foobar');
});
test('should pad with a custom character', () => {
expect(leftpad('foo', 5, '0')).toBe('00foo');
});
test('should return the original string when target length is 0', () => {
expect(leftpad('foo', 0)).toBe('foo');
});
test('should handle numeric input by converting it to a string', () => {
expect(leftpad(123, 5, '0')).toBe('00123');
});
test('should handle null input by converting it to the string "null"', () => {
expect(leftpad(null, 6)).toBe(' null');
});
test('should handle undefined input by converting it to the string "undefined"', () => {
expect(leftpad(undefined, 12)).toBe(' undefined');
});
test('should pad an empty string', () => {
expect(leftpad('', 4, 'x')).toBe('xxxx');
});
});
console.log('\n' + __passed + ' passed, ' + __failed + ' failed');
process.exit(__failed > 0 ? 1 : 0);
{
"name": "left-pad-lecture",
"version": "1.0.0",
"scripts": {
"test": "jest",
"dev": "node main.js"
},
"devDependencies": {
"jest": "^30.2.0"
}
}
Solution
// Naive ES2017 replacement — drops the String() coercion.
// Six of nine tests pass; the numeric / null / undefined tests
// throw because (n).padStart, (null).padStart, and (undefined).padStart
// are all undefined.
module.exports = leftpad;
function leftpad (str, length, paddingCharacter = ' ') {
return str.padStart(length, paddingCharacter);
}
Why six of nine pass: String.prototype.padStart(targetLength, padString) matches the original module’s main contract — default pad is ' ', no-truncation when already long enough, single-character padding to a wider target, empty-string handling. The standard library was designed to absorb this use case.
Why three break: (123).padStart, null.padStart, and undefined.padStart are all undefined (or throw, in the case of null / undefined). Numbers do not inherit from String.prototype. The original module hid this by calling String(str) first; the naive swap discards that line. The failing tests’ messages read roughly TypeError: str.padStart is not a function — the JS engine’s polite way of saying “you called a string method on a non-string.”
Step 3 restores the coercion in a way that keeps the one-liner spirit.
Step 2 — Knowledge Check
Min. score: 80%
1. Why does the naive replacement return str.padStart(length, paddingCharacter); cause leftpad(123, 5, '0') to throw rather than return '00123'?
JavaScript’s method dispatch follows the prototype chain. 123.__proto__ is Number.prototype, which has no padStart. The expression (123).padStart evaluates to undefined, and undefined('0') throws TypeError: str.padStart is not a function. The fix (step 3) is to coerce to string first — String(123) has padStart via String.prototype.
2. Six of the nine original tests still pass after the naive swap. Which observation best explains why the standard-library replacement is a legitimate candidate even though three tests break?
The strength of a refactoring candidate is judged by what it gets right by default. padStart matches the hard parts (default pad character, no-truncation, repeated padding to fill the gap) with zero glue code. Only one behavior — implicit input coercion — needs to be added back. That’s a one-line fix, which is exactly what step 3 demonstrates.
Restore the Contract — `String(str).padStart(length, paddingCharacter)`
Why this matters
The failing tests in step 2 weren’t a flaw in padStart — they were a flaw in our refactor. The original module had two responsibilities: coerce the input to a string, and pad it. We replaced the second with a built-in but dropped the first. Re-adding the coercion takes one function call, brings every test back to green, and produces the punch line of the lecture: the eleven-line module that broke the internet is now a one-liner.
🎯 You will learn to
- Apply
String(value)as an explicit coercion idiom - Synthesize a contract-preserving refactor that keeps the entire test suite green
- Evaluate the trade-off between micro-dependencies and standard-library evolution
The fix
function leftpad (str, length, paddingCharacter) {
return String(str).padStart(length, paddingCharacter);
}
String(value) — called without new — is the standard explicit-coercion idiom. It is identical in result to the original module’s str = String(str) line. With the coercion in place, leftpad(123, 5, '0') runs as String(123).padStart(5, '0') → '123'.padStart(5, '0') → '00123'. All nine tests pass.
The before/after, side by side
// BEFORE — the famous eleven lines (well, eight here).
function leftpad (str, length, paddingCharacter = ' ') {
str = String(str);
length = length - str.length;
if (length < 1) {
return str;
}
str = paddingCharacter.repeat(length) + str;
return str;
}
// AFTER — one line of ES2017.
function leftpad (str, length, paddingCharacter) {
return String(str).padStart(length, paddingCharacter);
}
✏️ Predict before you run
Does the one-line version also default the pad character to ' ' when paddingCharacter is undefined?
- (a) No —
padStart(length, undefined)pads with'undefined'repeated. - (b) No —
padStart(length, undefined)throwsTypeError. - (c) Yes — the spec for
padStartsays “ifpadStringis undefined, use a single space.” - (d) Yes — but only because we still call
String(str), which secretly handles it.
Commit to a letter, then run.
Reveal (after running)
**(c)**. The ECMAScript spec for `String.prototype.padStart` explicitly defaults `padString` to `' '` when it's `undefined`. That's why the first test (`pad a short string with spaces to the specified length`) passes without us writing `paddingCharacter = ' '` in the function signature. The built-in is doing the work the original module did manually. (a) is what you'd get if you *explicitly* passed `'undefined'`. (b) only happens with `null`-coerced edge cases far outside the contract.The real-world lesson
- The standard library evolves. Three lines you write today may become one built-in five years from now. Watch the spec.
- Micro-dependencies have a cost. Every
npm installis a supply-chain decision. If you can replace a dependency with a one-line built-in, you reduce maintenance, attack surface, and bundle size. - Read the spec, not just the docs.
padStart’s default-' 'rule is in TC39; without it, the one-liner wouldn’t preserve the contract.
Task
- In
left-pad.js, replace the body ofleftpadwithreturn String(str).padStart(length, paddingCharacter); - Click ▶ Run. The number argv now works again (numbers are coerced to strings).
- Click ✓ Test My Work.
- Confirm all nine tests pass.
- Look at
left-pad.jsonce more — that’s the entire module. Sit with that.
// 🛠️ TASK: Replace the body of leftpad with:
//
// return String(str).padStart(length, paddingCharacter);
//
// Then click ▶ Run and ✓ Test My Work. All nine tests should pass.
module.exports = leftpad;
function leftpad (str, length, paddingCharacter) {
return str.padStart(length, paddingCharacter); // still naive — fix me!
}
// main.js — same driver as step 1.
const leftpad = require('./left-pad.js');
const argv = process.argv.slice(2);
const numbers = argv.length > 0 ? argv : [124, 1, 90093, 9193];
const len = 5;
for (const num of numbers) {
console.log(leftpad(num, len, ' '));
}
// Same Jest-style contract tests as steps 1 and 2 — unchanged.
// The point is to watch the same suite confirm correctness
// across three different implementations.
let __passed = 0, __failed = 0;
function describe(name, fn) { console.log(name); fn(); }
function test(name, fn) {
try { fn(); console.log(' ✓ ' + name); __passed++; }
catch (e) { console.log(' ✗ ' + name); console.log(' ' + e.message); __failed++; }
}
function expect(actual) {
return { toBe(expected) {
if (!Object.is(actual, expected)) {
throw new Error('Expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual));
}
}};
}
const leftpad = require('../left-pad.js');
describe('leftpad', () => {
test('should pad a short string with spaces to the specified length', () => {
expect(leftpad('foo', 5)).toBe(' foo');
});
test('should return the original string if its length is equal to the target length', () => {
expect(leftpad('foobar', 6)).toBe('foobar');
});
test('should return the original string if its length is greater than the target length', () => {
expect(leftpad('foobar', 3)).toBe('foobar');
});
test('should pad with a custom character', () => {
expect(leftpad('foo', 5, '0')).toBe('00foo');
});
test('should return the original string when target length is 0', () => {
expect(leftpad('foo', 0)).toBe('foo');
});
test('should handle numeric input by converting it to a string', () => {
expect(leftpad(123, 5, '0')).toBe('00123');
});
test('should handle null input by converting it to the string "null"', () => {
expect(leftpad(null, 6)).toBe(' null');
});
test('should handle undefined input by converting it to the string "undefined"', () => {
expect(leftpad(undefined, 12)).toBe(' undefined');
});
test('should pad an empty string', () => {
expect(leftpad('', 4, 'x')).toBe('xxxx');
});
});
console.log('\n' + __passed + ' passed, ' + __failed + ' failed');
process.exit(__failed > 0 ? 1 : 0);
{
"name": "left-pad-lecture",
"version": "1.0.0",
"scripts": {
"test": "jest",
"dev": "node main.js"
},
"devDependencies": {
"jest": "^30.2.0"
}
}
Solution
// The eleven-line module that broke the internet, rewritten
// in one line of ES2017. Same contract, same tests, fewer
// moving parts.
module.exports = leftpad;
function leftpad (str, length, paddingCharacter) {
return String(str).padStart(length, paddingCharacter);
}
Why this is correct:
String(str)— the explicit coercion idiom. Called withoutnew(which would build a wrapper object). For any value ofstr,String(str)returns a primitive string:String(123)→'123',String(null)→'null',String(undefined)→'undefined'. This is exactly what the original module did on its first line..padStart(length, paddingCharacter)— the ES2017 built-in. DefaultpaddingCharacteris' 'per spec, no truncation when already long enough, repeatspaddingCharacteras needed to fill the gap.- One line. Sit with that for a moment.
The contract the tests encode is preserved end-to-end. The implementation moved from 11 lines of user code in 2016 to 1 line of language built-in in 2017. Three takeaways:
- Watch the spec. New ECMAScript revisions ship every June; standard-library additions can eliminate whole packages.
- Be skeptical of micro-dependencies.
npm install left-padwas the right call in 2014 and the wrong call in 2017 — same code, different ecosystem. - Keep the tests. The test suite outlived three implementations. That’s not a coincidence — that’s what tests are for.
Step 3 — Knowledge Check
Min. score: 80%
1. Why does the lecture write String(str) instead of new String(str)?
String(value) (no new) is the canonical explicit-coercion idiom — it returns a primitive string. new String(value) is a constructor call that wraps the primitive in a boxed String object; the wrapper has all the prototype methods (so wrapperObj.padStart(...) would work), but it fails === against primitives and behaves surprisingly in boolean contexts (new String('') is truthy). Almost every JavaScript style guide recommends String(value) for this reason.
2. (Spaced review — step 1) What does leftpad('foobar', 3) return under all three implementations the lecture walks through?
The “no truncation when already long enough” rule is a core part of the leftpad contract — and conveniently, String.prototype.padStart follows the same rule. That alignment is why padStart is a drop-in candidate at all. If padStart truncated, the standard library wouldn’t be a viable replacement.
3. (Spaced review — step 2) Six of the nine tests passed under the naive return str.padStart(length, paddingCharacter) swap. The lecture argues this still makes padStart a legitimate replacement candidate. Which of these is the strongest reason?
The three failing tests isolate one piece of glue — restoring String(str). The hard parts of the contract — defaulting the pad character, not truncating, repeating the pad to fill the gap — are absorbed by the spec-compliant built-in for free. That’s what makes padStart a true replacement: minimal glue, maximal correctness inherited from the standard library.