1 The Problem
We want a small program that converts a temperature from Celsius to Fahrenheit, or the other way around. The user picks which direction, types a number, and the program does the maths and shows the answer. Simple — but it teaches the core shape of every calculator-style tool: ask, calculate, show.
2 How to Think About It
Before any code, think about the two things the program needs to know and the two formulas it might use. You do not need TypeScript yet — just the plan:
3 The Build — explained part by part
Here is the complete converter. 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";
// cToF converts Celsius to Fahrenheit.
export function cToF(celsius: number): number {
return (celsius * 9) / 5 + 32;
}
// fToC converts Fahrenheit to Celsius.
export function fToC(fahrenheit: number): number {
return ((fahrenheit - 32) * 5) / 9;
}
// ask prints a prompt and waits for one line of input. We read lines through
// the readline interface's own async iterator (rl's "for await" protocol)
// instead of its question()/promises API — pulled one at a time with
// it.next(), that iterator is the one part of readline that reliably keeps
// up when several prompts are answered in a row, typed by hand or piped in
// from a file.
function makeAsk(rl: readline.Interface) {
const it = rl[Symbol.asyncIterator]();
return async (prompt: string): Promise<string> => {
stdout.write(prompt);
const { value, done } = await it.next();
return done ? "" : value;
};
}
async function main(): Promise<void> {
const rl = readline.createInterface({ input: stdin, terminal: false });
const ask = makeAsk(rl);
console.log("Temperature Converter");
console.log("1. Celsius to Fahrenheit");
console.log("2. Fahrenheit to Celsius");
const choice = (await ask("Choose 1 or 2: ")).trim();
if (choice === "1") {
const celsius = Number((await ask("Enter temperature in Celsius: ")).trim());
if (Number.isNaN(celsius)) {
console.log("Please type a number.");
} else {
console.log(`${celsius}°C is ${cToF(celsius)}°F`);
}
} else if (choice === "2") {
const fahrenheit = Number((await ask("Enter temperature in Fahrenheit: ")).trim());
if (Number.isNaN(fahrenheit)) {
console.log("Please type a number.");
} else {
console.log(`${fahrenheit}°F is ${fToC(fahrenheit)}°C`);
}
} else {
console.log("Please choose 1 or 2.");
}
rl.close();
}
if (require.main === module) {
main();
}(celsius: number): number says “takes a number, returns a number” — TypeScript checks every call site against that promise.readline.createInterface + an async iterator — Node has no built-in
input(). We open a readline interface on stdin and pull one line at a time from its own async iterator (rl[Symbol.asyncIterator]()), awaiting it.next() each time we need an answer.Number(answer.trim()) — converts the typed text to a number the same way Python’s
float() does; unlike Python, a bad string does not throw — it silently becomes NaN, so we check with Number.isNaN(...) before using it.celsius * 9 / 5 + 32 — identical maths to every other language here; JavaScript follows the same operator precedence.
if (require.main === module) — only runs
main() when this file is executed directly, not when another file requires it (handy for the tests, which import cToF/fToC without starting the interactive program).
rl.question() (the readline/promises API) twice in a row and expecting both typed answers to show up.question() can silently drop a line. Pulling lines from the interface’s own async iterator, one .next() per prompt, does not have this gap.Number("abc") to throw, the way Python’s float("abc") does.NaN instead. Always check Number.isNaN(value) before using a number you parsed from text..trim() on a line read from stdin — a trailing \r on Windows-style input makes "1\r" !== "1".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 { cToF, fToC } from "./temperature-converter";
test("freezing point: 0C is 32F", () => {
assert.equal(cToF(0), 32);
});
test("boiling point: 100C is 212F", () => {
assert.equal(cToF(100), 212);
});
test("reverse freezing: 32F is 0C", () => {
assert.equal(fToC(32), 0);
});
test("round trip returns the original number", () => {
assert.ok(Math.abs(fToC(cToF(25)) - 25) < 0.00001);
});Compile with npx tsc then run node --test converter.test.js (or skip the compile step while developing with npx tsx --test converter.test.ts). We pulled the formulas into small, independently-typed functions (cToF, fToC) so the tests can call them directly, without any menu or typed input in the way.
5 The Interface
The program’s interface — what it asks for and what it gives back — documented plainly.
What it expects
Choose 1 or 2: 1
Enter temperature in Celsius: 100What it returns
100.0°C is 212.0°F6 Run It & Automate It
Save the code as converter.ts. Compile it with npx tsc and run the result with node converter.js — or skip the separate compile step while experimenting with npx tsx converter.ts, which type-checks and runs in one command.
npx tsc converter.ts && node converter.jsPick a direction, type a temperature, and see the conversion.
A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.
Temperature Converter
1. Celsius to Fahrenheit
2. Fahrenheit to Celsius
Choose 1 or 2: 1
Enter temperature in Celsius: 100
100°C is 212°FNumber(" 100 ") is fine once trimmed, but Number("100cm") is NaN.echo "1\n100" | node converter.jsrl.question() twice.// 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 converter.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 temperature converter. Extend it:
- Add Kelvin. Offer Celsius→Kelvin too (Kelvin = Celsius + 273.15). (Teaches: a third
ifbranch.) - Round differently. Use
.toFixed(1)for a fixed one-decimal answer, orMath.round(...)for a whole number. (Teaches: Number formatting methods.) - Keep converting. Wrap the whole thing in a loop so the user can do many conversions without restarting. (Teaches: an outer
whileloop around async code.) - Guard bad input harder. Instead of giving up on a bad number, loop back and ask again. (Teaches: retry logic with
while.)
number parameters, NaN-checking, and testing maths against known facts with Node’s built-in test runner. Related reference: Basic Types, The TypeScript Compiler.