Introduction
This is my first article as a Java engineer learning TypeScript from scratch.
I've been writing Java professionally, but I got interested in modern tech stacks and started learning TypeScript and Python. My approach is simple: build small projects one by one, and write honestly about everything — what I struggled with, what I figured out, and what I asked AI to help with.
This article is for people on a similar learning journey. It's not a showcase of perfect code — it's an honest record of the process.
My Learning Style (AI Transparency)
I use Claude Pro (for design discussions and Q&A) and Cursor Pro (for coding support) as learning companions.
However, I follow these rules for myself:
- I write all the code myself — I never ask AI to write code for me
- AI helps with hints, spec clarification, and bug spotting
- I make sure I understand why something works before moving on
In this article, I clearly separate "what I implemented myself" from "what I asked AI for."
What I Built
A CLI tool that calculates BMI from height and weight input.
$ npm start
Enter your height (cm): 170
Enter your weight (kg): 70
BMI result: 24.22 (Normal)
BMI Classification
| BMI | Label |
|---|---|
| < 18.5 | Underweight |
| 18.5 – 24.9 | Normal |
| ≥ 25.0 | Obese |
📦 Repository: https://github.com/uya0526-design/bmi-calculator
Project Structure
bmi-calculator/
├── src/
│ ├── index.ts # Entry point / CLI I/O
│ ├── calculator.ts # BMI calculation and classification logic
│ ├── types.ts # Type definitions
│ └── __tests__/
│ ├── calculator.test.ts # Unit tests
│ └── types.test.ts # Type tests (skipped with describe.skip)
├── package.json
├── tsconfig.json
└── LEARNING_LOG.md
Separating responsibilities by file made it much clearer where everything lived.
Tech Stack
- TypeScript
- Node.js (
readlinemodule) - Vitest (unit testing)
What I Implemented Myself
types.ts — Type Definitions
// Type aliases for height, weight, and BMI value
type Height = number;
type Weight = number;
type BmiValue = number;
// Union type for classification labels
type BmiLabel = "Underweight" | "Normal" | "Obese";
// Object type to hold the calculation result
export type BmiOutput = {
bmi: BmiValue;
label: BmiLabel;
};
The key decision here was using a union type for BmiLabel. Any string outside of "Underweight" | "Normal" | "Obese" causes a type error at compile time.
calculator.ts — Calculation Logic
import type { BmiOutput } from "./types";
function getBmiLabel(bmi: number): string {
if (bmi < 18.5) return "Underweight";
if (bmi < 25) return "Normal";
return "Obese";
}
export function calculateBmi(height: number, weight: number): BmiOutput {
const heightInM = height / 100;
const bmi = weight / heightInM ** 2;
const label = getBmiLabel(bmi);
return { bmi, label }; // shorthand property notation
}
I decided not to export getBmiLabel since it's only used internally — and I made that call myself.
index.ts — CLI Input/Output
import * as readline from "readline";
import { calculateBmi } from "./calculator";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function main() {
rl.question("Enter your height (cm): ", (heightInput) => {
if (isNaN(Number(heightInput))) {
console.log("Please enter a number.");
rl.close();
return;
}
rl.question("Enter your weight (kg): ", (weightInput) => {
if (isNaN(Number(weightInput))) {
console.log("Please enter a number.");
rl.close();
return;
}
const result = calculateBmi(Number(heightInput), Number(weightInput));
console.log(`BMI result: ${result.bmi.toFixed(2)} (${result.label})`);
rl.close(); // ← position matters (see "Where I Got Stuck")
});
});
}
main();
calculator.test.ts — Unit Tests with Vitest
import { describe, it, expect } from "vitest";
import { calculateBmi } from "../calculator";
describe("calculateBmi", () => {
it("BMI 18.49 → Underweight", () => {
const result = calculateBmi(170, 53.5);
expect(result.label).toBe("Underweight");
});
it("BMI 18.5 → Normal", () => {
const result = calculateBmi(170, 53.52);
expect(result.label).toBe("Normal");
});
it("BMI 25 or above → Obese", () => {
const result = calculateBmi(170, 72.25);
expect(result.label).toBe("Obese");
});
it("BMI calculation accuracy", () => {
const result = calculateBmi(170, 70);
expect(result.bmi).toBeCloseTo(24.22, 1);
});
});
I deliberately chose boundary values (18.49 / 18.5 / 25) to verify the branching logic.
What I Asked AI For
| Topic | Details |
|---|---|
| Type design thinking | Asked about the difference between tuple types and object types |
| tsconfig.json options | Learned what module, target, and strict each do |
| Vitest setup | Confirmed the setup steps and package.json config |
| Test design principles | Learned the difference between toBe and toBeOneOf, and the one-case-per-test rule |
Where I Got Stuck
1. npm wouldn't run in PowerShell
Cause: The default execution policy was Restricted (no scripts allowed).
Fix:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
Using -Scope Process applies the change only to the current session — no permanent system changes.
2. Mismatch between package.json "type" and tsconfig.json "module"
Situation: package.json had "type": "module" but tsconfig.json had "module": "commonjs" — they were out of sync.
Fix: Changed package.json to "type": "commonjs". These two settings always need to match.
3. rl.close() in the wrong place
Situation: I put rl.close() at the end of the outer callback, but it was closing readline before the inner rl.question could finish.
Insight: rl.question is asynchronous. Writing code after it doesn't mean it runs after the callback completes.
Fix: Moved rl.close() inside the inner callback, after all processing is done.
What I Learned
TypeScript
| Topic | Key Takeaway |
|---|---|
| Union types | Union types restrict strings to allowed values, catching mistakes at compile time |
| Limits of type aliases |
Height = number and Weight = number are both just number at runtime — swapping arguments doesn't cause a type error (Branded Types can fix this) |
import type |
Explicitly imports types only — useful with verbatimModuleSyntax
|
| Shorthand properties |
{ bmi, label } works when variable names match property names |
Testing (Vitest)
| Topic | Key Takeaway |
|---|---|
| TypeScript types don't exist at runtime | Testing types with Vitest is unnecessary — the compiler already guarantees them |
toBe vs toBeOneOf
|
toBeOneOf passes if any value matches — it can't verify correctness. Use toBe with a specific expected value |
| Boundary value testing | Testing around thresholds (18.5, 25) verifies that branching logic is correct |
describe.skip |
Keeps the file in place while skipping tests — useful for preserving learning notes |
Wrapping Up
This was my first TypeScript project — a simple BMI calculator CLI.
Two things stood out from this experience:
- Designing types first made implementation smoother
- Async callbacks are easy to misplace — position matters
Next up: a Rock-Paper-Scissors game (union types, conditionals, enums).
The full learning log is in LEARNING_LOG.md.
This article is part of my public learning journey using AI tools (Claude Pro / Cursor Pro). All code is written by me — AI is used for design discussions, bug hints, and spec clarification only.
























