1 The Problem
We want a tool that creates a strong password: a random mix of letters, numbers, and symbols, of a length the user chooses. It teaches building a string from random choices — and an important lesson about which randomness is safe for security.
2 How to Think About It
Think about how a strong password is built, before any code:
3 The Build — explained part by part
Here is the complete generator. Read each part’s note below — you should understand the whole thing from the notes alone.
import * as readline from "node:readline";
import { stdin, stdout } from "node:process";
import { randomInt } from "node:crypto";
// Build the pool of characters a password can use.
const LOWER = "abcdefghijklmnopqrstuvwxyz";
const UPPER = LOWER.toUpperCase();
const DIGITS = "0123456789";
const SYMBOLS = "!@#$%^&*";
export const POOL = LOWER + UPPER + DIGITS + SYMBOLS;
// generate picks `length` random characters from the pool, securely —
// randomInt comes from Node's crypto module, not Math.random().
export function generate(length: number): string {
let password = "";
for (let i = 0; i < length; i++) {
password += POOL[randomInt(0, POOL.length)];
}
return password;
}
async function main(): Promise<void> {
const rl = readline.createInterface({ input: stdin, terminal: false });
const it = rl[Symbol.asyncIterator]();
stdout.write("Password length: ");
const answer = (await it.next()).value ?? "";
const length = Number(answer.trim());
rl.close();
const password = generate(length);
console.log(`Your password: ${password}`);
}
if (require.main === module) {
main();
}string.ascii_letters) makes it obvious exactly which characters a password can contain.import { randomInt } from "node:crypto" — the same secure generator used by the number-guessing game, but here it matters for real:
Math.random() is not cryptographically secure and must never be used to generate anything resembling a password or a secret.POOL[randomInt(0, POOL.length)] — pick one random index into the pool string, repeated
length times. randomInt(0, n) returns a whole number from 0 up to (but not including) n — exactly the valid index range for a string of length n.export function generate(length: number): string — pulling the loop into its own typed function (rather than writing it inline in
main) is what makes it directly testable, with no prompt or typed input in the way.
Math.random() to pick password characters.Math.random() is not designed to be unpredictable to an attacker. Always use crypto.randomInt (or crypto.randomBytes) for anything security-sensitive.Math.floor(Math.random() * POOL.length) as a “good enough” substitute for randomInt.randomInt avoids both problems.length > 0 before generating — see “Try this next” below.4 Test & Prove Each Part
How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.
import { test } from "node:test";
import assert from "node:assert/strict";
import { generate, POOL } from "./password-generator";
test("a length-12 request gives 12 characters", () => {
assert.equal(generate(12).length, 12);
});
test("every character comes from the allowed pool", () => {
for (const ch of generate(50)) {
assert.ok(POOL.includes(ch));
}
});
test("two passwords are (almost) never identical", () => {
assert.notEqual(generate(16), generate(16));
});Compile with npx tsc then run node --test password.test.js. Testing randomness directly is impossible, so instead we test its guarantees: the right length, only pool characters, and (practically) no repeats.
5 The Interface
What it expects
Password length: 16What it returns
Your password: Rxp7QarCT*DvpdFA6 Run It & Automate It
Save the code as password.ts, compile with npx tsc, and run with node password.js — or run it directly with npx tsx password.ts.
npx tsc password.ts && node password.jsType a length and get back a secure random password of that length.
A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.
Password length: 16
Your password: JK5O&aFMvc3s3GWCPOOL really does combine letters, digits, and symbols — a typo that drops one of the four building blocks silently shrinks the pool.RangeError: The value of "max" is out of rangerandomInt requires its upper bound to be a positive integer greater than the lower bound; this fires if POOL is accidentally empty.// Jenkinsfile — runs the type-checker and tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Node') {
steps {
sh 'node --version' // confirm Node is installed
sh 'npm install -D typescript @types/node' // zero runtime deps — just the compiler and its Node types
}
}
stage('Type-check and test') {
steps {
sh 'npx tsc --noEmit' // catch type errors before anything runs
sh 'npx tsc' // compile to plain JavaScript
sh 'node --test password.test.js' // Node's built-in test runner, no extra install needed
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
You have a working password generator. Extend it:
- Reject a non-positive length. Print a clear error instead of silently returning an empty string. (Teaches: input validation.)
- Let the user opt out of symbols. Some sites do not accept them. (Teaches: building the pool conditionally.)
- Guarantee variety. Force at least one digit and one symbol to appear. (Teaches: combining a guarantee with randomness.)
- Estimate strength. Print how many possible passwords exist for that pool and length (
pool.length ** length). (Teaches:BigIntfor numbers too large fornumber.)
crypto.randomInt (not Math.random) is the right tool whenever randomness has to be unpredictable, plus how to pull a generator into a small, directly-testable, typed function. Related reference: Basic Types, The TypeScript Compiler.