# Building a BMI Calculator CLI with TypeScript — Types, Functions, and Vitest

> Source: <https://dev.to/uya0526design/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest-2dbb>
> Published: 2026-05-23 16:05:50+00:00

## 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) andCursor 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
whysomething works before moving onIn 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.

``` bash
$ 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](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 (
`readline`

module) - 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

``` python
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

``` js
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

``` js
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](https://github.com/uya0526-design/bmi-calculator/blob/main/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.*
