Before you begin
Why this matters
Most of a real program is code you did not write. Knowing what the language already has saves you from writing a weaker copy, and reading what a function promises lets you predict its result for empty or unusual input before a user finds it.
What this helps you reason about
- Spot a hand-written helper that a built-in function already does correctly.
- Predict what a library function does at the edges, from its documentation.
- Read a package.json, and explain why updating a package can change your program.
A mountain railway shows numbers and names on its screens: 1,234,567 riders this year, and a route from Brook to Summit. A few lines of your own code can format them, and so can functions that come with the language. This lesson compares the two, shows how to read what a library function promises, and how a project adds code that other people wrote.
Already know this? The short version
- JavaScript’s built-ins (Math, String, Array, Intl, JSON, Date) and the Node.js modules are the standard library; a hand-written helper misses edges that they handle, such as a thousands separator that puts a comma after a minus sign.
- Read the contract, not the name: replace with a string pattern changes the first match alone, and replaceAll changes all of them.
- npm install adds a package to package.json with a range such as ^4.1.0; package-lock.json records the exact versions, and npm ci installs exactly those.
- Semantic versioning is a promise, not a check: a MINOR or PATCH update can still change behavior your code relied on, and so can a newer engine.
Words in this lesson (7)
- standard library
- The functions and objects that come with a language, with nothing to install. In TypeScript, these are JavaScript’s built-in objects, such as Math, String, Array, and Intl, and what Node.js or the browser adds.
- contract
- What a function promises: the input it accepts, the result it gives, and what it does at the edges, such as empty input. The function’s documentation states it.
- third-party package
- Code written outside your project and outside the team that makes the language, which a project adds by name and version.
- dependency
- A package that a project needs in order to build or run. The project lists each dependency, with the versions it accepts, in a file.
- import
- A line that makes names from another module usable in this file, such as import { goal } from "./steps".
- semantic versioning
- A rule for version numbers written MAJOR.MINOR.PATCH, such as 4.1.0. The authors promise that a new PATCH only fixes bugs, a new MINOR adds features, and a new MAJOR may break code written for the old version.
- lock file
- A file that records the exact version of every package a project uses, so that every computer builds with the same ones. npm writes package-lock.json.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Standard library
What does the standard library already give you?
Every language comes with a standard library: functions and objects you can use without installing anything. In TypeScript, it is JavaScript’s built-in objects, such as Math, String, Array, and Intl, plus what Node.js or the browser adds.
A small job, such as putting commas into a large number, is easy to write yourself. It is also easy to get almost right. The function below groups the digits of 1234567 correctly. For -250000 it gives -,250,000, because it treats the minus sign as a digit. toLocaleString("en-US") is the library’s version. It writes a number the way people in the United States do, and it already handles signs, decimals, and very large values.
Before you write a helper, look for it in the standard library. The library’s version has been used by many programs, so its edge cases have been found and fixed.
function withCommas(n: number): string {
const digits = String(n);
let text = "";
for (let i = 0; i < digits.length; i++) {
if (i > 0 && (digits.length - i) % 3 === 0) text = text + ",";
text = text + digits[i];
}
return text;
}
withCommas(-250000); // "-,250,000"
(-250000).toLocaleString("en-US"); // "-250,000" Look in the standard library before you write a helper; its version already handles the edge cases.
Coming from Python: There is no import line for built-ins such as Math or Intl: they are global names in every file.
Precisely: Intl and the language setting
toLocaleString() without an argument uses the computer’s language setting, so one program can print 1,234,567 on one computer and 1.234.567 on another. Passing "en-US" fixes the format. Node.js has included the data for every language since version 13.
Experiment
Format a rider count two ways
withCommas puts a comma before every third character from the end, and it treats every character as a digit. The minus sign is a character too, so a negative number gets a comma after the sign. toLocaleString("en-US") formats the number as a number, with the sign kept apart.
Why it matters A hand-written helper is often right for the values you tried first. Look for the library’s version before you write one.
Predict both printed lines. Then change 1234567 to -250000 and predict again.
// Puts a comma before each group of three digits.
function withCommas(n: number): string {
const digits = String(n);
let text = "";
for (let i = 0; i < digits.length; i++) {
if (i > 0 && (digits.length - i) % 3 === 0) {
text = text + ",";
}
text = text + digits[i];
}
return text;
}
const riders = 1234567;
console.log(withCommas(riders));
console.log(riders.toLocaleString("en-US"));
What to look for
At the first stop inside withCommas, digits and text show not initialized yet. Then digits shows "1234567", and text grows one character at a time: "1", "1,", "1,2", and so on, up to "1,234,567". The stepper often stops twice on the for line; that is normal for this debugger. The run prints 1,234,567 twice. After the edit, digits shows "-250000", and on the second pass text becomes "-,": the loop counted the minus sign as a digit. The run prints -,250,000, then -250,000.
Expected output
1,234,567
1,234,56702 / Contracts
What does a library function promise?
A library function has a contract: the input it accepts, the result it gives, and what it does at the edges, such as empty input or a value that is not there. The function’s documentation states the contract. For JavaScript’s built-ins, MDN Web Docs is the usual reference, and the ECMAScript specification is the exact one.
The name of a function is not its contract. route.replace("-", " > ") sounds as if it replaces every dash. Its contract says that a string pattern replaces the first match and nothing more. replaceAll replaces every match.
When you read a contract, look for four things: what the function returns, what it does with empty input, what happens when nothing matches, and whether it changes its argument or returns a new value. A string method always returns a new string; route itself does not change.
const route = "Brook-Mill-Tunnel-Summit";
route.replace("-", " > "); // "Brook > Mill-Tunnel-Summit"
route.replaceAll("-", " > "); // "Brook > Mill > Tunnel > Summit" Read the contract, not the name: the result, empty input, no match, and whether the argument changes.
Coming from Python: Python’s str.replace replaces every match unless you pass a count; JavaScript’s replace with a string replaces the first match alone.
Precisely: replace with a regular expression
replace also accepts a regular expression, a pattern written between slashes. With the g flag, as in route.replace(/-/g, " > "), it replaces every match. replaceAll was added in ECMAScript 2021; the stepper’s compiler accepts it because it checks against the ES2022 library.
Experiment
Replace the dashes in a route
When the pattern is a string, replace changes the first match and stops. replaceAll changes every match. Neither one changes route: a string never changes, and both return a new one.
Why it matters A name such as replace does not tell you how many matches it changes. The documentation does.
Predict the printed sign. Then change replace to replaceAll and predict again.
const route = "Brook-Mill-Tunnel-Summit";
const sign = route.replace("-", " > ");
console.log(sign);
What to look for
At the first stop, route and sign show not initialized yet. After line 2, sign shows "Brook > Mill-Tunnel-Summit", and route still shows "Brook-Mill-Tunnel-Summit": replace returned a new string and left route alone. The run prints Brook > Mill-Tunnel-Summit. After the edit, sign shows "Brook > Mill > Tunnel > Summit".
Expected output
Brook > Mill-Tunnel-Summit03 / Packages
How do you add a package someone else wrote?
A third-party package is code written outside your project and outside the team that makes your language. Node.js projects get packages from the npm registry with the npm tool. npm install date-fns downloads the date-fns package into a folder called node_modules and adds it to package.json. That file lists the project’s dependencies: the packages it needs to build or run.
After that, the package is a module like any other. An import names it by its package name instead of a file path, as Modules, packages, and namespaces showed for files. Which of its names a package lets you use is the subject of Exports, visibility, and encapsulation. The stepper runs one file and cannot install anything, so the code below is for reading only.
// package.json (part of it)
{
"dependencies": {
"date-fns": "^4.1.0"
}
}
// trip.ts
import { addDays } from "date-fns";
console.log(addDays(new Date("2026-10-13"), 1)); package.json names each dependency; npm installs it; an import then uses it by its package name.
Coming from Python: npm installs packages into the project’s own node_modules folder, not for the whole computer, so two projects can use two versions of one package.
Precisely: types for a package
A package written in TypeScript ships its own type declarations. For a package written in plain JavaScript, the types often come from a second package under @types, such as @types/node for Node.js itself. The stepper has no @types/node, which is why its programs cannot use process without declaring it.
04 / Versions
Why can a new version change your program?
Packages change. Most packages number their versions by semantic versioning: MAJOR.MINOR.PATCH. The authors promise that a new PATCH only fixes bugs, a new MINOR adds features, and a new MAJOR may break code written for the old version.
In package.json, "^4.1.0" accepts any version from 4.1.0 up to, but not including, 5.0.0. So two computers that install at different times can get different versions. npm prevents that with a lock file, package-lock.json, which records the exact version of every package it installed. npm ci installs exactly those versions.
A new version can still change what your program does. A bug fix changes a result that your code depended on. An edge case that the contract never promised starts to behave differently. Update one dependency at a time, on purpose, and run your tests. The engine is part of this too: replaceAll exists since ECMAScript 2021, and an older engine does not have it.
// package-lock.json (part of it)
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz"
} A version range lets updates in; the lock file decides which exact versions you get.
Precisely: what the range symbols allow
^ allows any change that keeps the first number that is not zero: ^0.3.1 accepts 0.3.x but not 0.4.0. ~4.1.0 accepts 4.1.x alone. npm install may update package-lock.json when package.json changes; npm ci never changes it, and fails when the two files disagree.
05 / Try it
Can you explain a fresh example?
Read the contract of the last argument before you predict.
const parts = "Brook,Mill,Summit".split(",", 2);
console.log(parts.length, parts[1]);
What is printed?
Reveal the reasoning
2 Mill. The second argument of split is a limit: the result has at most that many pieces, and the rest of the text is dropped. It does not mean “cut into two parts”. In Java, a call that looks the same keeps the rest in the last piece, Mill,Summit: same name, different contract.
Practice tickets
- USIP-101 · Monthly passes for February 2100 end on a day that does not exist → Free
- USIP-102 · Locomotives running alone are listed with one wagon → Sign in · Pro
- USIP-103 · City circle bus announcements never say “and” before the last stop → Sign in · Pro
Keep exploring
Errors as values or exceptions →