{"slug": "building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest", "title": "Building a BMI Calculator CLI with TypeScript — Types, Functions, and Vitest", "summary": "This article documents a Java engineer's process of learning TypeScript by building a BMI calculator CLI tool, with the author writing all code themselves while using AI for guidance and debugging. The project implements TypeScript features including union types for BMI classification labels, separate modules for calculator logic and CLI input/output, and unit tests using Vitest with boundary value testing. The author emphasizes honest documentation of their learning journey, including what they implemented independently versus what they asked AI for assistance with.", "body_md": "## Introduction\n\nThis is my first article as a Java engineer learning TypeScript from scratch.\n\nI'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.\n\nThis 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.\n\n## My Learning Style (AI Transparency)\n\nI use\n\nClaude Pro(for design discussions and Q&A) andCursor Pro(for coding support) as learning companions.However, I follow these rules for myself:\n\nI write all the code myself— I never ask AI to write code for me- AI helps with hints, spec clarification, and bug spotting\n- I make sure I understand\nwhysomething works before moving onIn this article, I clearly separate \"what I implemented myself\" from \"what I asked AI for.\"\n\n## What I Built\n\nA CLI tool that calculates BMI from height and weight input.\n\n``` bash\n$ npm start\n\nEnter your height (cm): 170\nEnter your weight (kg): 70\nBMI result: 24.22 (Normal)\n```\n\n**BMI Classification**\n\n| BMI | Label |\n|---|---|\n| < 18.5 | Underweight |\n| 18.5 – 24.9 | Normal |\n| ≥ 25.0 | Obese |\n\n📦 Repository: [https://github.com/uya0526-design/bmi-calculator](https://github.com/uya0526-design/bmi-calculator)\n\n## Project Structure\n\n```\nbmi-calculator/\n├── src/\n│   ├── index.ts              # Entry point / CLI I/O\n│   ├── calculator.ts         # BMI calculation and classification logic\n│   ├── types.ts              # Type definitions\n│   └── __tests__/\n│       ├── calculator.test.ts # Unit tests\n│       └── types.test.ts      # Type tests (skipped with describe.skip)\n├── package.json\n├── tsconfig.json\n└── LEARNING_LOG.md\n```\n\nSeparating responsibilities by file made it much clearer where everything lived.\n\n## Tech Stack\n\n- TypeScript\n- Node.js (\n`readline`\n\nmodule) - Vitest (unit testing)\n\n## What I Implemented Myself\n\n### types.ts — Type Definitions\n\n```\n// Type aliases for height, weight, and BMI value\ntype Height = number;\ntype Weight = number;\ntype BmiValue = number;\n\n// Union type for classification labels\ntype BmiLabel = \"Underweight\" | \"Normal\" | \"Obese\";\n\n// Object type to hold the calculation result\nexport type BmiOutput = {\n  bmi: BmiValue;\n  label: BmiLabel;\n};\n```\n\nThe key decision here was using a **union type** for `BmiLabel`\n\n. Any string outside of `\"Underweight\" | \"Normal\" | \"Obese\"`\n\ncauses a type error at compile time.\n\n### calculator.ts — Calculation Logic\n\n``` python\nimport type { BmiOutput } from \"./types\";\n\nfunction getBmiLabel(bmi: number): string {\n  if (bmi < 18.5) return \"Underweight\";\n  if (bmi < 25) return \"Normal\";\n  return \"Obese\";\n}\n\nexport function calculateBmi(height: number, weight: number): BmiOutput {\n  const heightInM = height / 100;\n  const bmi = weight / heightInM ** 2;\n  const label = getBmiLabel(bmi);\n  return { bmi, label };  // shorthand property notation\n}\n```\n\nI decided not to `export`\n\n`getBmiLabel`\n\nsince it's only used internally — and I made that call myself.\n\n### index.ts — CLI Input/Output\n\n``` js\nimport * as readline from \"readline\";\nimport { calculateBmi } from \"./calculator\";\n\nconst rl = readline.createInterface({\n  input: process.stdin,\n  output: process.stdout,\n});\n\nfunction main() {\n  rl.question(\"Enter your height (cm): \", (heightInput) => {\n    if (isNaN(Number(heightInput))) {\n      console.log(\"Please enter a number.\");\n      rl.close();\n      return;\n    }\n    rl.question(\"Enter your weight (kg): \", (weightInput) => {\n      if (isNaN(Number(weightInput))) {\n        console.log(\"Please enter a number.\");\n        rl.close();\n        return;\n      }\n      const result = calculateBmi(Number(heightInput), Number(weightInput));\n      console.log(`BMI result: ${result.bmi.toFixed(2)} (${result.label})`);\n      rl.close();  // ← position matters (see \"Where I Got Stuck\")\n    });\n  });\n}\n\nmain();\n```\n\n### calculator.test.ts — Unit Tests with Vitest\n\n``` js\nimport { describe, it, expect } from \"vitest\";\nimport { calculateBmi } from \"../calculator\";\n\ndescribe(\"calculateBmi\", () => {\n  it(\"BMI 18.49 → Underweight\", () => {\n    const result = calculateBmi(170, 53.5);\n    expect(result.label).toBe(\"Underweight\");\n  });\n\n  it(\"BMI 18.5 → Normal\", () => {\n    const result = calculateBmi(170, 53.52);\n    expect(result.label).toBe(\"Normal\");\n  });\n\n  it(\"BMI 25 or above → Obese\", () => {\n    const result = calculateBmi(170, 72.25);\n    expect(result.label).toBe(\"Obese\");\n  });\n\n  it(\"BMI calculation accuracy\", () => {\n    const result = calculateBmi(170, 70);\n    expect(result.bmi).toBeCloseTo(24.22, 1);\n  });\n});\n```\n\nI deliberately chose boundary values (18.49 / 18.5 / 25) to verify the branching logic.\n\n## What I Asked AI For\n\n| Topic | Details |\n|---|---|\n| Type design thinking | Asked about the difference between tuple types and object types |\n| tsconfig.json options | Learned what `module` , `target` , and `strict` each do |\n| Vitest setup | Confirmed the setup steps and `package.json` config |\n| Test design principles | Learned the difference between `toBe` and `toBeOneOf` , and the one-case-per-test rule |\n\n## Where I Got Stuck\n\n### 1. `npm`\n\nwouldn't run in PowerShell\n\n**Cause:** The default execution policy was `Restricted`\n\n(no scripts allowed).\n\n**Fix:**\n\n```\nSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process\n```\n\nUsing `-Scope Process`\n\napplies the change only to the current session — no permanent system changes.\n\n### 2. Mismatch between `package.json`\n\n`\"type\"`\n\nand `tsconfig.json`\n\n`\"module\"`\n\n**Situation:** `package.json`\n\nhad `\"type\": \"module\"`\n\nbut `tsconfig.json`\n\nhad `\"module\": \"commonjs\"`\n\n— they were out of sync.\n\n**Fix:** Changed `package.json`\n\nto `\"type\": \"commonjs\"`\n\n. These two settings always need to match.\n\n### 3. `rl.close()`\n\nin the wrong place\n\n**Situation:** I put `rl.close()`\n\nat the end of the outer callback, but it was closing readline before the inner `rl.question`\n\ncould finish.\n\n**Insight:** `rl.question`\n\nis asynchronous. Writing code *after* it doesn't mean it runs *after* the callback completes.\n\n**Fix:** Moved `rl.close()`\n\ninside the inner callback, after all processing is done.\n\n## What I Learned\n\n### TypeScript\n\n| Topic | Key Takeaway |\n|---|---|\n| Union types | Union types restrict strings to allowed values, catching mistakes at compile time |\n| Limits of type aliases |\n`Height = number` and `Weight = number` are both just `number` at runtime — swapping arguments doesn't cause a type error (Branded Types can fix this) |\n`import type` |\nExplicitly imports types only — useful with `verbatimModuleSyntax`\n|\n| Shorthand properties |\n`{ bmi, label }` works when variable names match property names |\n\n### Testing (Vitest)\n\n| Topic | Key Takeaway |\n|---|---|\n| TypeScript types don't exist at runtime | Testing types with Vitest is unnecessary — the compiler already guarantees them |\n`toBe` vs `toBeOneOf`\n|\n`toBeOneOf` passes if any value matches — it can't verify correctness. Use `toBe` with a specific expected value |\n| Boundary value testing | Testing around thresholds (18.5, 25) verifies that branching logic is correct |\n`describe.skip` |\nKeeps the file in place while skipping tests — useful for preserving learning notes |\n\n## Wrapping Up\n\nThis was my first TypeScript project — a simple BMI calculator CLI.\n\nTwo things stood out from this experience:\n\n**Designing types first made implementation smoother****Async callbacks are easy to misplace — position matters**\n\nNext up: a **Rock-Paper-Scissors game** (union types, conditionals, enums).\n\nThe full learning log is in [LEARNING_LOG.md](https://github.com/uya0526-design/bmi-calculator/blob/main/LEARNING_LOG.md).\n\n*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.*", "url": "https://wpnews.pro/news/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest", "canonical_source": "https://dev.to/uya0526design/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest-2dbb", "published_at": "2026-05-23 16:05:50+00:00", "updated_at": "2026-05-23 16:33:28.120793+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["TypeScript", "Java", "Python", "Claude Pro", "Cursor Pro", "Vitest", "BMI Calculator CLI", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest", "markdown": "https://wpnews.pro/news/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest.md", "text": "https://wpnews.pro/news/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest.txt", "jsonld": "https://wpnews.pro/news/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest.jsonld"}}