<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Uya</title>
    <description>The latest articles on DEV Community by Uya (@uya0526design).</description>
    <link>https://dev.to/uya0526design</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947868%2F3e2ac0d9-ff74-4790-9883-956f75a1964e.png</url>
      <title>DEV Community: Uya</title>
      <link>https://dev.to/uya0526design</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uya0526design"/>
    <language>en</language>
    <item>
      <title>Building a Weight Tracker CLI in Python — Type Hints, Pure Functions, and pytest</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:56:39 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-weight-tracker-cli-in-python-type-hints-pure-functions-and-pytest-3f1k</link>
      <guid>https://dev.to/uya0526design/building-a-weight-tracker-cli-in-python-type-hints-pure-functions-and-pytest-3f1k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my eighth article as a Java engineer learning TypeScript and Python from scratch.&lt;/p&gt;

&lt;p&gt;My first seven articles were all small TypeScript CLIs. In the previous one, I built an HTTP client in TypeScript that calls a FastAPI (Python) server I wrote myself — that was my first contact with Python (FastAPI), but only as "the thing the client calls."&lt;/p&gt;

&lt;p&gt;In other words, Python played a supporting role last time. From here, &lt;strong&gt;I start Python properly from "project 1."&lt;/strong&gt; For this first one I go back to the basics: a &lt;strong&gt;weight tracker CLI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What I focused on this time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python basics (&lt;code&gt;#&lt;/code&gt; comments, &lt;code&gt;def&lt;/code&gt;, snake_case, f-strings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type hints&lt;/strong&gt; (&lt;code&gt;list[float]&lt;/code&gt;, &lt;code&gt;tuple[float, float, float]&lt;/code&gt;) and how they differ from Java Generics&lt;/li&gt;
&lt;li&gt;Input validation with &lt;code&gt;try / except ValueError&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extracting the calculation logic as a "pure function"&lt;/strong&gt; (separation of concerns, testability)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit testing with pytest&lt;/strong&gt; (normal, boundary, and error cases)&lt;/li&gt;
&lt;li&gt;The habits an ex-Java engineer slips into — and correcting them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As always, I write honestly about where I got stuck, what I thought through, and what I asked AI for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Where this sits in the series&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the first article in my Python series. It's also a chance to check whether the mindset I picked up in the TypeScript series — "think in types," "separate logic from I/O," "write tests" — still works when the language changes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Learning companions &amp;amp; how this article is written&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (coding support) as learning companions. Beyond the code, &lt;strong&gt;I also collaborate with AI on writing this article itself.&lt;/strong&gt; Stating the facts plainly here.&lt;/p&gt;

&lt;p&gt;Division of roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tech selection, design, implementation, and code verification&lt;/strong&gt; → me&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article structure, outline, draft prose, and translation&lt;/strong&gt; → in collaboration with Claude&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All content is checked and revised by me before publishing&lt;/strong&gt; → me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My guiding principle is "&lt;strong&gt;the thinking is mine, the wording is AI-assisted, and I verify all of it.&lt;/strong&gt;" I write the code myself (I never ask AI to write code for me), I use AI for hints, spec clarification, and bug spotting, and I make sure I understand &lt;em&gt;why&lt;/em&gt; before moving on — and I apply that same stance to the writing process.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I continue to clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Enter a menu number, and the CLI records a weight, lists recorded weights, or computes statistics (average, max, min).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--------------------------------
1. Record weight
2. Display the recorded weights
3. Display the weight calculation result
4. Exit
--------------------------------
Enter your choice: 1
Enter your weight(kg): 70.5
--------------------------------
Enter your choice: 3
Weight calculation result:
Average weight: 70.5
Max weight: 70.5
Min weight: 70.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data is held &lt;strong&gt;in memory&lt;/strong&gt; and resets when you exit (a deliberate simplification for local learning). I'm saving persistence for the next stage.&lt;/p&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/weight_tracker_py" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/weight_tracker_py&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  File Structure and Tech Stack
&lt;/h2&gt;

&lt;p&gt;The structure is very simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weight_tracker_py/
├── tests/
│   ├── __init__.py
│   └── test_main.py     # unit tests
├── main.py              # entry point
└── requirements.txt     # dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; 3.12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest&lt;/strong&gt; 9.0.3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow from creating a virtual environment (&lt;code&gt;venv&lt;/code&gt;) to running tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv               &lt;span class="c"&gt;# project-local isolated environment&lt;/span&gt;
.&lt;span class="se"&gt;\v&lt;/span&gt;&lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\A&lt;/span&gt;ctivate.ps1       &lt;span class="c"&gt;# activate (Windows PowerShell)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest                &lt;span class="c"&gt;# install the test tool&lt;/span&gt;
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt     &lt;span class="c"&gt;# record dependencies&lt;/span&gt;
python main.py                    &lt;span class="c"&gt;# run the app&lt;/span&gt;
pytest                            &lt;span class="c"&gt;# run the tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I understood &lt;code&gt;venv&lt;/code&gt; as "per-project isolation," close to a Maven local repository or Node's &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  main.py — The menu loop and function split
&lt;/h3&gt;

&lt;p&gt;I run the menu with &lt;code&gt;while True&lt;/code&gt; + &lt;code&gt;if / elif&lt;/code&gt;, dispatching to a function per choice, and &lt;code&gt;break&lt;/code&gt; on &lt;code&gt;4&lt;/code&gt; to exit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;display_main_menu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;record_weight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;display_recorded_weights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;display_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid choice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I understood &lt;code&gt;if __name__ == "__main__":&lt;/code&gt; as a declaration of "this is the program's entry point," close to Java's &lt;code&gt;public static void main(String[] args)&lt;/code&gt;. It doesn't run when imported — only when the file is run directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input validation — try / except ValueError
&lt;/h3&gt;

&lt;p&gt;The weight is read as a string, so I check whether it can be converted to a number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_weight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enter your weight(kg): &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if the weight is a number
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid weight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;recorded_weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first I tried to judge "is this a number?" with &lt;code&gt;isdigit()&lt;/code&gt; and got stuck (more below). Structurally this is almost the same as Java's &lt;code&gt;try-catch&lt;/code&gt;: a string that can't be converted by &lt;code&gt;float()&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt; (close to Java's &lt;code&gt;NumberFormatException&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing — enumerate and an empty-list check
&lt;/h3&gt;

&lt;p&gt;I used &lt;code&gt;enumerate&lt;/code&gt; to list items with numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_recorded_weights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recorded weights: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorded_weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No weights recorded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorded_weights&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enumerate&lt;/code&gt; is a loop that gives you the index and the element at once, letting me write what I used to write as &lt;code&gt;for (int i = 0; i &amp;lt; list.size(); i++)&lt;/code&gt; in Java much more cleanly. I wanted the display to start at 1, so I print &lt;code&gt;index + 1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Statistics — extracting a pure function (my biggest lesson this time)
&lt;/h3&gt;

&lt;p&gt;This was the design highlight of the project. At first I bundled display and calculation into one function, but I &lt;strong&gt;extracted just the calculation logic as a "pure function that takes arguments and only returns values."&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# display side (reads the global list → hard to test)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weight calculation result: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorded_weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No weights recorded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;average_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorded_weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average weight: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;average_weight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Max weight: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_weight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Min weight: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;min_weight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# calculation side (takes arguments, only returns values → easy to test)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;average_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;max_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;average_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two points here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Don't read the global variable directly — take it as an argument&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;display_...&lt;/code&gt; side reads the module-level &lt;code&gt;recorded_weights&lt;/code&gt;. That depends on the result of &lt;code&gt;input()&lt;/code&gt;, which makes it hard to test. The &lt;code&gt;calculate_...&lt;/code&gt; side takes &lt;code&gt;weights&lt;/code&gt; as an argument, so a test can pass any list it likes. This is separation of concerns — the same idea I kept coming back to in the TypeScript series, and it worked just as well in Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Return multiple values as a tuple&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;return average_weight, max_weight, min_weight&lt;/code&gt; returns three values at once. The caller can unpack them with &lt;code&gt;a, b, c = ...&lt;/code&gt;. In Java, "multiple return values" meant a dedicated class or an array, so writing it with plain syntax felt fresh.&lt;/p&gt;

&lt;p&gt;The return type hint &lt;code&gt;-&amp;gt; tuple[float, float, float]&lt;/code&gt; feels close to Generics like &lt;code&gt;List&amp;lt;Float&amp;gt;&lt;/code&gt; in Java. The big difference is that it's &lt;strong&gt;not enforced at runtime&lt;/strong&gt; (returning a value that doesn't match the type isn't an error), so I understood it as information for the reader and the IDE.&lt;/p&gt;

&lt;h3&gt;
  
  
  tests/test_main.py — normal, boundary, and error cases
&lt;/h3&gt;

&lt;p&gt;Because I'd extracted a pure function, the tests were very straightforward to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;calculate_weight_calculation_result&lt;/span&gt;

&lt;span class="c1"&gt;# normal: multiple items
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;70.12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;average_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;average_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;80.02&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;90.0&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;70.12345&lt;/span&gt;

&lt;span class="c1"&gt;# error: empty list → division by zero
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_weight_calculation_result_empty_list&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;

&lt;span class="c1"&gt;# boundary: a single item
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_weight_calculation_result_one_weight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;70.12345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;average_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_weight_calculation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;average_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;70.12&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;max_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;70.12345&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;min_weight&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;70.12345&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pytest.raises(ZeroDivisionError)&lt;/code&gt; corresponds to Java's &lt;code&gt;assertThrows(ArithmeticException.class, ...)&lt;/code&gt;. It's interesting that "this should throw an exception" can be written as a test too.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spec clarification&lt;/td&gt;
&lt;td&gt;Surfacing unclear points: input validation, display format, how to exit, menu design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment syntax&lt;/td&gt;
&lt;td&gt;Pointing out that Python uses &lt;code&gt;#&lt;/code&gt;, not &lt;code&gt;//&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Pointing out that &lt;code&gt;isdigit()&lt;/code&gt; can't handle decimals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversion handling&lt;/td&gt;
&lt;td&gt;Pointing out I was appending the raw string, and that double-converting with &lt;code&gt;float()&lt;/code&gt; was unnecessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design suggestion&lt;/td&gt;
&lt;td&gt;Suggesting I pass the global variable as an argument, for testability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type hints&lt;/td&gt;
&lt;td&gt;Pointing out a mismatch between &lt;code&gt;-&amp;gt; None&lt;/code&gt; and &lt;code&gt;-&amp;gt; list[float]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;pytest.raises&lt;/code&gt; syntax, and the ideal folder structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Explaining &lt;code&gt;requirements.txt&lt;/code&gt; (direct deps only vs. pinning everything)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;README&lt;/td&gt;
&lt;td&gt;Tidying it up in both Japanese and English&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note: I found and fixed the &lt;strong&gt;wrong test expectation (below) myself.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I wrote comments with &lt;code&gt;//&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I wrote a comment with &lt;code&gt;//&lt;/code&gt; and it wouldn't run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; A Java / TypeScript habit. Python comments use &lt;code&gt;#&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Changed them all to &lt;code&gt;#&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; When the language changes, the first differences show up in the tiniest syntax. A small thing, but it reliably reminds me "you're writing a different language now."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;isdigit()&lt;/code&gt; can't reject decimals
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I used &lt;code&gt;"70.5".isdigit()&lt;/code&gt; for input validation, and a valid weight got rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;isdigit()&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt; &lt;strong&gt;only when every character is a digit.&lt;/strong&gt; A decimal point &lt;code&gt;.&lt;/code&gt; makes it &lt;code&gt;False&lt;/code&gt;, so a value like &lt;code&gt;70.5&lt;/code&gt; doesn't pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; I dropped the character-type check and switched to trying a &lt;code&gt;float()&lt;/code&gt; conversion, catching failures with &lt;code&gt;try / except ValueError&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NG: a decimal point makes it False → rejects a valid weight
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# OK: try to convert, reject on failure
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid weight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; If you want to know "can this be used as a number?", &lt;strong&gt;actually trying to convert it&lt;/strong&gt; is more reliable than inspecting character types. I learned to pick the check that fits the goal.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. ZeroDivisionError on an empty list
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Computing the average with zero recorded weights crashed the program.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; In &lt;code&gt;sum(weights) / len(weights)&lt;/code&gt;, &lt;code&gt;len&lt;/code&gt; became &lt;code&gt;0&lt;/code&gt;, causing a division by zero (&lt;code&gt;ZeroDivisionError&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix and design decision:&lt;/strong&gt; I added an early return &lt;code&gt;if len(recorded_weights) == 0&lt;/code&gt; on the display side, returning "No weights recorded" to the user. On the other hand, I deliberately left the &lt;strong&gt;calculation function (&lt;code&gt;calculate_...&lt;/code&gt;) to raise &lt;code&gt;ZeroDivisionError&lt;/code&gt; as-is, and verified that with a test.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; "Where to prevent an error" and "where to let an exception through" are separate decisions. I block early near the UI, while letting the pure function raise naturally and pinning that behavior with a test.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Can't import main.py from tests/
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; &lt;code&gt;from main import ...&lt;/code&gt; in &lt;code&gt;tests/test_main.py&lt;/code&gt; raised &lt;code&gt;ModuleNotFoundError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;tests/&lt;/code&gt; wasn't recognized as a package, so the import couldn't be resolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Adding &lt;code&gt;tests/__init__.py&lt;/code&gt; (an empty file) resolved it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Folder structure is part of "the spec for making things run." Tracking down the cause myself was a small confidence boost.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. My test expectation was wrong
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; A test failed. The &lt;code&gt;assert&lt;/code&gt; for &lt;code&gt;average_weight&lt;/code&gt; didn't match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; It wasn't the implementation — &lt;strong&gt;my own expected value was wrong.&lt;/strong&gt; I'd written the &lt;code&gt;round()&lt;/code&gt; result from my head without actually computing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; The average of &lt;code&gt;[70.12345, 75, 80, 85, 90]&lt;/code&gt; is &lt;code&gt;400.12345 / 5 = 80.02469&lt;/code&gt;, and &lt;code&gt;round(..., 2)&lt;/code&gt; makes it &lt;code&gt;80.02&lt;/code&gt;. I recomputed it by hand and corrected the expectation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; &lt;strong&gt;"A failing test" doesn't always mean "the implementation is wrong."&lt;/strong&gt; Catching a wrong expectation is also part of a test's value. Experiencing the real-world cycle in miniature — a test fails, then you separate whether the cause is the implementation or the expectation — was my biggest gain this time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Python basics (mapped to Java)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Comments&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;#&lt;/code&gt; (&lt;code&gt;//&lt;/code&gt; is Java / JavaScript)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function definition&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;def name():&lt;/code&gt; (corresponds to Java's &lt;code&gt;static void&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naming&lt;/td&gt;
&lt;td&gt;snake_case &lt;code&gt;record_weight&lt;/code&gt; (vs. Java's camelCase)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple return values&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;return a, b, c&lt;/code&gt; returns a tuple (Java needs a dedicated class or array)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String formatting&lt;/td&gt;
&lt;td&gt;f-strings (&lt;code&gt;f"{variable}"&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Type hints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You can annotate arguments and return values (&lt;code&gt;list[float]&lt;/code&gt;, &lt;code&gt;tuple[float, float, float]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Feels close to Java Generics (&lt;code&gt;List&amp;lt;Float&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;But it's &lt;strong&gt;not enforced at runtime&lt;/strong&gt; — it's information for readability and IDE support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exception handling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;try / except&lt;/code&gt; has the same structure as Java's &lt;code&gt;try-catch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A string that &lt;code&gt;float()&lt;/code&gt; can't convert raises &lt;code&gt;ValueError&lt;/code&gt; (close to Java's &lt;code&gt;NumberFormatException&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testability (separation of concerns)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Functions containing &lt;code&gt;input()&lt;/code&gt; or &lt;code&gt;print()&lt;/code&gt; are hard to test&lt;/li&gt;
&lt;li&gt;Extracting the calculation logic as a &lt;strong&gt;pure function that takes arguments and returns values&lt;/strong&gt; makes it testable&lt;/li&gt;
&lt;li&gt;This is exactly the "separation of concerns" that matters in real work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Unit testing (pytest)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Position&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pytest&lt;/code&gt; corresponds to Java's &lt;code&gt;JUnit&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception tests&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;with pytest.raises(...)&lt;/code&gt; corresponds to &lt;code&gt;assertThrows(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;Covering normal, boundary, and error cases is the baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;When a test fails, it can also reveal a wrong expectation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Project structure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Putting &lt;code&gt;__init__.py&lt;/code&gt; in &lt;code&gt;tests/&lt;/code&gt; resolves pytest's import&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; can list "direct deps only" or "pin everything." When reproducibility matters, list everything&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;I built this the same way as before: write the code myself and have AI review it. As my first Python project, three things stood out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit of thinking in types carried straight over:&lt;/strong&gt; The "think in types first" I repeated in the TypeScript series came naturally with Python's type hints. There's the difference that they're not enforced at runtime, but understanding the goal — "write them for the reader and the IDE" — was a real gain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separation of concerns transcends the language:&lt;/strong&gt; Extracting the calculation logic as a pure function made the tests surprisingly easy to write. "Separate I/O from logic" works the same way in TypeScript and in Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The experience of a test failing is itself a lesson:&lt;/strong&gt; Recomputing and fixing my own expectation taught me that "a test isn't just a tool for doubting the implementation." Separating whether a failure comes from the implementation or the expectation was the most satisfying part.&lt;/p&gt;

&lt;p&gt;The ex-Java habits (&lt;code&gt;//&lt;/code&gt; comments, &lt;code&gt;isdigit()&lt;/code&gt;) did surface, but I corrected them along with the reasons, so I should be able to leave them behind for the next Python project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my record of building a weight tracker CLI in Python, centered on &lt;strong&gt;type hints, pure functions, and pytest&lt;/strong&gt; — my first article in the Python series.&lt;/p&gt;

&lt;p&gt;Continuous progress from the TypeScript series:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Picked up Python basics while staying aware of the differences from Java&lt;/strong&gt; (&lt;code&gt;#&lt;/code&gt;, &lt;code&gt;def&lt;/code&gt;, snake_case, tuple multiple returns)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrote type hints while understanding that they're not enforced at runtime&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extracted the calculation logic as a pure function, practicing separation of concerns in Python too&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Covered normal, boundary, and error cases with pytest&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Found and fixed a wrong test expectation myself, experiencing the same cycle as real work&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, I want to move into &lt;strong&gt;persistence&lt;/strong&gt; with file saving and JSON, and graduate from this project's "data disappears when you exit" simplification.&lt;/p&gt;

&lt;p&gt;The full learning log is in the repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uya0526-design/weight_tracker_py/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;weight_tracker_py / LEARNING_LOG.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article is part of my public learning journey using AI tools (Claude Pro / Cursor Pro). The thinking and all code are mine; I collaborate with AI on the writing (structure, drafting, translation) and verify every line before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytest</category>
      <category>beginners</category>
      <category>testing</category>
    </item>
    <item>
      <title>Building an HTTP Client CLI for Your Own API with TypeScript — FastAPI, POST, and Error Handling</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Sun, 07 Jun 2026 13:32:28 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-an-http-client-cli-for-your-own-api-with-typescript-fastapi-post-and-error-handling-2goo</link>
      <guid>https://dev.to/uya0526design/building-an-http-client-cli-for-your-own-api-with-typescript-fastapi-post-and-error-handling-2goo</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my seventh article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my previous article, I built a CLI that calls an external weather API (Open-Meteo) with &lt;code&gt;fetch&lt;/code&gt;, and learned about &lt;code&gt;async/await&lt;/code&gt;, response type definitions, and error handling with &lt;code&gt;response.ok&lt;/code&gt;. But that was "read-only (GET)" against "someone else's API." This time I go a step further: &lt;strong&gt;I build the API server myself and then build an HTTP client that calls it.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend: a REST API server built with &lt;strong&gt;FastAPI (Python)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Client: a &lt;strong&gt;TypeScript&lt;/strong&gt; CLI that calls that API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I focused on this time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementing &lt;strong&gt;GET and POST&lt;/strong&gt; with &lt;code&gt;fetch&lt;/code&gt; (previously GET only)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;&lt;code&gt;Content-Type: application/json&lt;/code&gt;&lt;/strong&gt; header on POST (a 422 error I fixed myself)&lt;/li&gt;
&lt;li&gt;How &lt;strong&gt;where you place try-catch&lt;/strong&gt; changes behavior (continue vs. exit)&lt;/li&gt;
&lt;li&gt;The difference between &lt;strong&gt;&lt;code&gt;return&lt;/code&gt; and &lt;code&gt;throw error&lt;/code&gt;&lt;/strong&gt; (close to Java's &lt;code&gt;throws&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Narrowing the &lt;code&gt;unknown&lt;/code&gt; type with &lt;strong&gt;&lt;code&gt;error instanceof Error&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI basics (&lt;strong&gt;Spring-like decorators&lt;/strong&gt;, &lt;code&gt;BaseModel&lt;/code&gt;, &lt;code&gt;HTTPException&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as always — I write honestly about where I got stuck, what I thought through, and what I asked AI for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Scope of this article&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article centers on the TypeScript client. The Python (FastAPI) side is introduced compactly, as "the thing the client calls."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Learning companions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;My rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Pick a menu option in the CLI, and it gets a list of items, gets an item by ID, or adds an item — all against my own API server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Custom&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;Call&lt;/span&gt; &lt;span class="nx"&gt;CLI&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;
&lt;span class="o"&gt;================================&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="nx"&gt;ID&lt;/span&gt;
&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Add&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;
&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Exit&lt;/span&gt;
&lt;span class="o"&gt;================================&lt;/span&gt;
&lt;span class="nx"&gt;Choose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;banana&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cherry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data is held &lt;strong&gt;in memory&lt;/strong&gt; on the server, so it resets when the server restarts (a deliberate simplification for local learning).&lt;/p&gt;

&lt;p&gt;📦 Repositories&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client (TypeScript): &lt;a href="https://github.com/uya0526-design/simple_api_ts" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/simple_api_ts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Server (Python / FastAPI): &lt;a href="https://github.com/uya0526-design/simple_api_py" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/simple_api_py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  System Overview
&lt;/h2&gt;

&lt;p&gt;This project is split across two repositories. The roles are simply "server and client."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ TypeScript CLI ]  --- HTTP (fetch) ---&amp;gt;  [ FastAPI server ]
  simple_api_ts                              simple_api_py
  - GET  /items                              - holds items in memory
  - GET  /items/{id}                         - accepts GET / POST
  - POST /items                              - returns 404 via HTTPException
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint spec:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns all items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/items/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns one item (404 if not found)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adds an item (id auto-assigned by the server)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Backend: FastAPI Server (Python)
&lt;/h2&gt;

&lt;p&gt;First I need something to call. This was my first time touching FastAPI, but to an ex-Java engineer's eyes it looked &lt;strong&gt;remarkably close to a Spring Boot controller.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv               &lt;span class="c"&gt;# project-local isolated environment&lt;/span&gt;
.&lt;span class="se"&gt;\v&lt;/span&gt;&lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\A&lt;/span&gt;ctivate.ps1       &lt;span class="c"&gt;# activate (Windows PowerShell)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;fastapi uvicorn       &lt;span class="c"&gt;# install packages&lt;/span&gt;
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt     &lt;span class="c"&gt;# record dependencies&lt;/span&gt;
uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;         &lt;span class="c"&gt;# start the dev server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I understood &lt;code&gt;venv&lt;/code&gt; as "per-project isolation," close to a Maven local repository or Node's &lt;code&gt;node_modules&lt;/code&gt;, and &lt;code&gt;uvicorn main:app --reload&lt;/code&gt; as the dev server equivalent of TypeScript's &lt;code&gt;ts-node&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the endpoints
&lt;/h3&gt;

&lt;p&gt;Routes are defined in &lt;code&gt;main.py&lt;/code&gt;. The decorator feel is just like Spring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# internal / response use (has id)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="c1"&gt;# request-receiving use (no id — auto-assigned by server)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cherry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items/{item_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ItemCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;new_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_item&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Splitting RequestDTO and ResponseDTO
&lt;/h3&gt;

&lt;p&gt;The main design decision was &lt;strong&gt;separating the receiving type (&lt;code&gt;ItemCreate&lt;/code&gt;) from the internal/return type (&lt;code&gt;Item&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ItemCreate&lt;/code&gt;: only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;. No &lt;code&gt;id&lt;/code&gt; (the client shouldn't send it)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Item&lt;/code&gt;: includes &lt;code&gt;id&lt;/code&gt;. The server assigns it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoiding "id can go in either one" and making each type's responsibility clear — I realized this is the same idea as separating RequestDTO and ResponseDTO in Java.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java mapping (FastAPI to Spring)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FastAPI / Python&lt;/th&gt;
&lt;th&gt;Equivalent Java concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@app.get("/items")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@GetMapping("/items")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@app.post("/items")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@PostMapping("/items")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;{item_id}&lt;/code&gt; path parameter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@PathVariable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DTO class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;item: ItemCreate&lt;/code&gt; (typed argument)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HTTPException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ResponseStatusException&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raise&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;throw&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;len(items)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.size()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ts-node&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/docs&lt;/code&gt; (auto Swagger UI)&lt;/td&gt;
&lt;td&gt;manual setup like SpringFox&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest surprise was that just visiting &lt;code&gt;/docs&lt;/code&gt; auto-generates a Swagger UI — coming from setting that up by hand, that felt like magic.&lt;/p&gt;

&lt;p&gt;That's the server done. Now for the main event: the TypeScript client.&lt;/p&gt;




&lt;h2&gt;
  
  
  Client: TypeScript CLI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  types.ts — Mirror the server's types
&lt;/h3&gt;

&lt;p&gt;The client types mirror the server's DTOs. Dropping &lt;code&gt;id&lt;/code&gt; from &lt;code&gt;ItemCreate&lt;/code&gt; follows the same reasoning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ItemCreate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// no id — the server auto-assigns it&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;An interface closes with &lt;code&gt;}&lt;/code&gt;, not &lt;code&gt;};&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first I wrote the end of the &lt;code&gt;interface&lt;/code&gt; as &lt;code&gt;};&lt;/code&gt;. The correct form is &lt;code&gt;}&lt;/code&gt; (watch the difference from a &lt;code&gt;class&lt;/code&gt; or a &lt;code&gt;function&lt;/code&gt; expression). The trailing &lt;code&gt;;&lt;/code&gt; on properties works with or without it, but by TypeScript convention it's common to include it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  api.ts — GET and POST with fetch
&lt;/h3&gt;

&lt;p&gt;Previously I only did GET; this time I add POST too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ItemCreate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getItemById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ItemCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputItem&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I improved from AI feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Collect the base URL into a constant&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first I hardcoded &lt;code&gt;"http://localhost:8000"&lt;/code&gt; in all three functions. I pulled it into a single &lt;code&gt;const API_ENDPOINT&lt;/code&gt; to make changes easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The POST body is fine as &lt;code&gt;JSON.stringify(inputItem)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first I expanded the fields: &lt;code&gt;JSON.stringify({ name: inputItem.name, price: inputItem.price })&lt;/code&gt;. But since &lt;code&gt;inputItem&lt;/code&gt; is the &lt;code&gt;ItemCreate&lt;/code&gt; type and is guaranteed to hold only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;JSON.stringify(inputItem)&lt;/code&gt; is enough. &lt;strong&gt;The type makes the code shorter&lt;/strong&gt; — a nice example.&lt;/p&gt;

&lt;h3&gt;
  
  
  index.ts — Interactive menu and input validation
&lt;/h3&gt;

&lt;p&gt;I wrap &lt;code&gt;readline&lt;/code&gt; in a &lt;code&gt;Promise&lt;/code&gt; and run a menu loop with &lt;code&gt;while (true)&lt;/code&gt; + &lt;code&gt;switch&lt;/code&gt; (an application of what I built up in previous projects).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter ID: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part here is &lt;strong&gt;where to place try-catch&lt;/strong&gt; (more below). In "get item by ID" (&lt;code&gt;case "2"&lt;/code&gt;), the CLI was exiting entirely when the server returned 404, so I added &lt;code&gt;try-catch&lt;/code&gt; inside the &lt;code&gt;case&lt;/code&gt; to keep the loop going.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter ID: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getItemById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;404&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Item not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// delegate unexpected errors to main().catch()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  api.test.ts — Mocking fetch (success and failure)
&lt;/h3&gt;

&lt;p&gt;Seven tests total. I reuse &lt;code&gt;jest.spyOn(global, "fetch")&lt;/code&gt; from last time. This time, for the failure case, I used &lt;code&gt;mockRejectedValue&lt;/code&gt; to reproduce &lt;code&gt;fetch&lt;/code&gt; itself failing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;throws when the API returns an error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockRejectedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;network error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreAllMocks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the add (POST) test, I &lt;strong&gt;compared the count (length) before and after&lt;/strong&gt; to confirm exactly one item was added.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; PASS  src/__tests__/api.test.ts

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Time:        0.248 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Unit vs. integration tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests that complete with mocks are "unit tests"; tests that need the actual FastAPI server running are "integration tests." Right now they share a single describe block, but as things grow, splitting them into &lt;code&gt;unit/&lt;/code&gt; and &lt;code&gt;integration/&lt;/code&gt; folders makes them easier to manage.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spec clarification&lt;/td&gt;
&lt;td&gt;Surfacing missing and unclear requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design suggestion (Python)&lt;/td&gt;
&lt;td&gt;Explaining why to split &lt;code&gt;ItemCreate&lt;/code&gt; and &lt;code&gt;Item&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concept explanation (Python)&lt;/td&gt;
&lt;td&gt;Explaining &lt;code&gt;venv&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;uvicorn&lt;/code&gt;, &lt;code&gt;HTTPException&lt;/code&gt; vs Java/TS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 exit bug&lt;/td&gt;
&lt;td&gt;Caught that &lt;code&gt;case "2"&lt;/code&gt; had no try-catch, so errors reached &lt;code&gt;main().catch()&lt;/code&gt; and exited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error message branching&lt;/td&gt;
&lt;td&gt;Suggested &lt;code&gt;error.message.includes("404")&lt;/code&gt; to show "not found"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;return vs throw&lt;/td&gt;
&lt;td&gt;Suggested re-throwing with &lt;code&gt;throw error&lt;/code&gt; instead of &lt;code&gt;return&lt;/code&gt; on unexpected errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base URL collection&lt;/td&gt;
&lt;td&gt;Suggested pulling the three hardcoded URLs into an &lt;code&gt;API_ENDPOINT&lt;/code&gt; constant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body simplification&lt;/td&gt;
&lt;td&gt;Suggested simplifying to &lt;code&gt;JSON.stringify(inputItem)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;interface syntax&lt;/td&gt;
&lt;td&gt;Caught the trailing &lt;code&gt;};&lt;/code&gt; that should be &lt;code&gt;}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note: I found and fixed the &lt;strong&gt;422 error (below) myself.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A 422 error on POST (the Content-Type header)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; When adding an item (POST), the server returned 422 (Unprocessable Entity).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; My &lt;code&gt;fetch&lt;/code&gt; POST didn't set the &lt;code&gt;Content-Type: application/json&lt;/code&gt; header. Without it, FastAPI can't interpret the request body as JSON and returns 422.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ no header — the body can't be read as JSON → 422&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputItem&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ declare the Content-Type&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputItem&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; I reached the cause on my own. A server and a client only agree to "exchange JSON" once that's declared via the header. I never thought about this while writing GET-only code.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The CLI exits entirely on a 404
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Entering a non-existent ID exited the CLI with no error message at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The exception thrown by &lt;code&gt;getItemById&lt;/code&gt; wasn't caught inside the &lt;code&gt;switch&lt;/code&gt; and propagated all the way to &lt;code&gt;main().catch()&lt;/code&gt;. Since &lt;code&gt;main().catch()&lt;/code&gt; is the last line of defense that ends the program, the loop couldn't resume and the CLI fell over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ no catch in the case → the exception reaches main().catch() and exits&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getItemById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 404 throws → loop breaks → exit&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added &lt;code&gt;try-catch&lt;/code&gt; inside &lt;code&gt;case "2"&lt;/code&gt;, showing "Item not found" for 404 and continuing the loop (code shown earlier).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; &lt;strong&gt;Where you catch a thrown error decides whether the program continues or exits.&lt;/strong&gt; The same exception leads to a different user experience depending on where you place the catch.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. The difference between &lt;code&gt;return&lt;/code&gt; and &lt;code&gt;throw error&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; On unexpected errors, I first exited main with &lt;code&gt;return&lt;/code&gt;. That quietly ended the program normally, and I couldn't tell what had happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause and fix:&lt;/strong&gt; &lt;code&gt;return&lt;/code&gt; just ends main normally. Logging with &lt;code&gt;console.error(error)&lt;/code&gt; and then re-throwing with &lt;code&gt;throw error&lt;/code&gt; lets it reach &lt;code&gt;main().catch()&lt;/code&gt; and end with &lt;code&gt;process.exit(1)&lt;/code&gt; (abnormal exit).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// normal exit. no error detail remains&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// abnormal exit. delegate to main().catch() → process.exit(1)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; This felt close to delegating an exception upward with Java's &lt;code&gt;throws&lt;/code&gt;. Choosing deliberately between "swallow it" and "throw it up" matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  async/await and fetch (GET / POST)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;POST specifies &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt;, and &lt;code&gt;body&lt;/code&gt;. The &lt;code&gt;body&lt;/code&gt; is stringified with &lt;code&gt;JSON.stringify(...)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sending JSON to FastAPI requires &lt;code&gt;Content-Type: application/json&lt;/code&gt; (without it, 422)&lt;/li&gt;
&lt;li&gt;For both GET and POST, you detect errors yourself with &lt;code&gt;!response.ok&lt;/code&gt; and &lt;code&gt;throw&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Error handling
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;try-catch placement&lt;/td&gt;
&lt;td&gt;Where you catch decides whether the program continues or exits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error instanceof Error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A caught &lt;code&gt;error&lt;/code&gt; is &lt;code&gt;unknown&lt;/code&gt;. Narrow the type before using &lt;code&gt;.message&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;return&lt;/code&gt; vs &lt;code&gt;throw error&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;return&lt;/code&gt; exits normally. &lt;code&gt;throw error&lt;/code&gt; delegates to &lt;code&gt;main().catch()&lt;/code&gt; for an abnormal exit. Close to Java's &lt;code&gt;throws&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Letting types simplify code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Since the &lt;code&gt;ItemCreate&lt;/code&gt; type is guaranteed to hold only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;JSON.stringify(inputItem)&lt;/code&gt; sends it as-is. No need to expand the fields by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FastAPI / Python (first contact)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decorators&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@app.get&lt;/code&gt; / &lt;code&gt;@app.post&lt;/code&gt; correspond to Spring's &lt;code&gt;@GetMapping&lt;/code&gt; / &lt;code&gt;@PostMapping&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines request/response data structures. Equivalent to a Java DTO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HTTPException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An exception with a status code. Equivalent to &lt;code&gt;ResponseStatusException&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RequestDTO/ResponseDTO split&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ItemCreate&lt;/code&gt; (no id) and &lt;code&gt;Item&lt;/code&gt; (with id) separate the responsibilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Just visiting it auto-generates a Swagger UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Test design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tests that complete with mocks are unit tests; those needing a running server are integration tests&lt;/li&gt;
&lt;li&gt;A POST test can confirm the add by comparing the count (length) before and after&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;I built this the same way as before: write the code myself and have AI review it. The big difference from last time was that I &lt;strong&gt;built the "other side" of the API myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three things stood out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A server and a client are connected by a "contract":&lt;/strong&gt; The 422 error came down to declaring "this is JSON" via the &lt;code&gt;Content-Type&lt;/code&gt; header before FastAPI would understand it. Building both sides myself made that boundary agreement tangible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exceptions are a design choice about "where to catch":&lt;/strong&gt; The 404-exit incident showed me that try-catch placement directly shapes the user experience. The &lt;code&gt;return&lt;/code&gt; vs &lt;code&gt;throw error&lt;/code&gt; distinction also clicked, connecting back to my memory of Java's &lt;code&gt;throws&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FastAPI looked like Spring:&lt;/strong&gt; Decorators, DTOs, exception classes, auto-docs. It was rewarding to feel my past Java experience working as a foundation even in a new language.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my record of building a self-made FastAPI server and a TypeScript HTTP client CLI that calls it.&lt;/p&gt;

&lt;p&gt;Progress since the previous projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Implemented not just GET but POST with &lt;code&gt;fetch&lt;/code&gt; (&lt;code&gt;Content-Type&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Found and fixed the cause of a 422 error myself&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understood that try-catch placement changes behavior, and made a CLI that doesn't fall over on 404&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used &lt;code&gt;return&lt;/code&gt; and &lt;code&gt;throw error&lt;/code&gt; deliberately&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built the server side with FastAPI too, understanding both ends of the API&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building both ends of an API once gave me both perspectives — "the server as seen from the client" and "the client as seen from the server." Next, I want to build on this setup and move toward more practical data operations and persistence.&lt;/p&gt;

&lt;p&gt;Full learning logs are in each repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uya0526-design/simple_api_ts/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;simple_api_ts / LEARNING_LOG.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uya0526-design/simple_api_py/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;simple_api_py / LEARNING_LOG.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>python</category>
      <category>fastapi</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Weather API CLI with TypeScript — async/await, fetch, and Response Types</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Sat, 06 Jun 2026 01:32:45 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-weather-api-cli-with-typescript-asyncawait-fetch-and-response-types-3182</link>
      <guid>https://dev.to/uya0526design/building-a-weather-api-cli-with-typescript-asyncawait-fetch-and-response-types-3182</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my sixth article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my previous article, I built a Quiz CLI and learned about &lt;code&gt;enum&lt;/code&gt;, tuple types, and mocking an entire module with &lt;code&gt;jest.mock()&lt;/code&gt;. This time, I built a &lt;strong&gt;Weather API CLI&lt;/strong&gt; and focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling an external API asynchronously with &lt;strong&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Describing an &lt;strong&gt;API response with type definitions&lt;/strong&gt; (&lt;code&gt;type&lt;/code&gt; to model the JSON shape)&lt;/li&gt;
&lt;li&gt;HTTP error handling with &lt;strong&gt;&lt;code&gt;response.ok&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Extracting object key types with &lt;strong&gt;&lt;code&gt;keyof typeof&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Swapping out &lt;code&gt;fetch&lt;/code&gt; with &lt;strong&gt;&lt;code&gt;jest.spyOn(global, "fetch")&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Verifying structure and type at once with &lt;strong&gt;&lt;code&gt;toMatchObject&lt;/code&gt; + &lt;code&gt;expect.any()&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as always — I write honestly about where I got stuck, what I thought through, and what I asked AI for.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Learning companions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;My rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI weather tool. Pick a city, or enter a latitude and longitude, and it shows the current weather, temperature, humidity, and wind speed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Weather API Call CLI
================================================
1. Select a city and run
2. Enter latitude and longitude and run
3. Exit
Choose: 1
1. Sapporo
2. Sendai
3. Tokyo
4. Nagoya
5. Osaka
6. Fukuoka
7. Kagoshima
8. Naha
Choose a city: 3
Tokyo weather: Partly cloudy
Tokyo temperature: 20.4°C
Tokyo humidity: 80%
Tokyo wind speed: 3.1km/h
================================================
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weather data comes from the free &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; API (no API key required).&lt;/p&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/weather_api" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/weather_api&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weather_api/
├── src/
│   ├── index.ts             # Entry point / CLI menu
│   ├── api.ts               # API call logic
│   ├── types.ts             # Type definitions
│   └── __tests__/
│       └── api.test.ts      # Unit tests
├── jest.config.js
├── tsconfig.json
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three-layer structure as the previous projects: &lt;code&gt;types.ts&lt;/code&gt; (types) / &lt;code&gt;api.ts&lt;/code&gt; (logic) / &lt;code&gt;index.ts&lt;/code&gt; (UI). New this time: &lt;strong&gt;communicating with an external API&lt;/strong&gt; is the main event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Jest + ts-jest (unit testing)&lt;/li&gt;
&lt;li&gt;Open-Meteo API (weather data)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  types.ts — API Response Types
&lt;/h3&gt;

&lt;p&gt;The heart of the type design this time was &lt;strong&gt;how to express an API response as a type&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  First, define the "city data"
&lt;/h4&gt;

&lt;p&gt;I defined the list of selectable cities and their latitude, longitude, and timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// List of selectable cities&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cityList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sapporo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sendai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Nagoya&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Osaka&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fukuoka&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Kagoshima&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Naha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Latitude / longitude / timezone per city&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cityCoordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;35.68123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;139.76712&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quietly useful tool here is &lt;strong&gt;&lt;code&gt;keyof typeof&lt;/code&gt;&lt;/strong&gt;. It lets me pull out just the keys of &lt;code&gt;cityCoordinates&lt;/code&gt; (the city names) as a type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CityName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;cityCoordinates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// becomes a union type: "Sapporo" | "Sendai" | "Tokyo" | ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Java I'd define the cities as an &lt;code&gt;enum&lt;/code&gt;. In TypeScript I can reuse the keys of an object that already exists directly as a type — that felt fresh.&lt;/p&gt;

&lt;h4&gt;
  
  
  Define the API response type
&lt;/h4&gt;

&lt;p&gt;This was the main challenge. I called the API first, checked the actual response JSON, and modeled that shape directly with &lt;code&gt;type&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WeatherResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timezone_abbreviation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;current_units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;relative_humidity_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;weather_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;relative_humidity_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;weather_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;A type that didn't match reality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first I defined &lt;code&gt;temperature_2m&lt;/code&gt; and the other fields inside &lt;code&gt;current&lt;/code&gt; as &lt;code&gt;string&lt;/code&gt;. Looking at the actual API response, they're numbers, so &lt;code&gt;number&lt;/code&gt; is correct — and AI caught this for me (more below). A wrong type definition leads to runtime errors, so I felt firsthand how important it is to check the type against the real response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Convert weather codes to text
&lt;/h4&gt;

&lt;p&gt;Open-Meteo returns WMO (World Meteorological Organization) weather codes as numbers. I defined a mapping to convert them to readable text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WeatherCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mainly clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Partly cloudy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cloudy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thunderstorm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;I first tried writing this as an enum&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I originally went for &lt;code&gt;enum&lt;/code&gt;, but I decided on my own that a "code → text" mapping reads more naturally as a &lt;code&gt;const&lt;/code&gt; object than as an &lt;code&gt;enum&lt;/code&gt;. An &lt;code&gt;enum&lt;/code&gt; is "a named set of constants," while an object is "a key-to-value mapping" — the difference in purpose started to feel intuitive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point I had written the keys as the string &lt;code&gt;"0"&lt;/code&gt;, which didn't line up with the numeric &lt;code&gt;weather_code&lt;/code&gt; from the API, so I fixed them to the numeric key &lt;code&gt;0&lt;/code&gt; (also an AI catch).&lt;/p&gt;




&lt;h3&gt;
  
  
  api.ts — Async API Calls with fetch
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;getWeather&lt;/code&gt; function takes a latitude, longitude, and timezone, and calls the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WeatherResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timezone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.open-meteo.com/v1/forecast?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two key points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Detect HTTP errors with &lt;code&gt;response.ok&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt; does not throw on HTTP errors like 404 or 500 — &lt;code&gt;response.ok&lt;/code&gt; just becomes &lt;code&gt;false&lt;/code&gt; (this differs from how some Java HTTP clients behave). So I have to check &lt;code&gt;if (!response.ok)&lt;/code&gt; myself and throw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Branch when &lt;code&gt;timezone&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the flow where you enter latitude/longitude directly, there may be no &lt;code&gt;timezone&lt;/code&gt;, so I branch to leave it out of the query in that case.&lt;/p&gt;




&lt;h3&gt;
  
  
  index.ts — Wrapping readline in a Promise
&lt;/h3&gt;

&lt;p&gt;I adopted the &lt;code&gt;async/await&lt;/code&gt; wrapping I learned in the previous Quiz project, from the start this time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets me write &lt;code&gt;await askQuestion(...)&lt;/code&gt; top-to-bottom inside the &lt;code&gt;while (true)&lt;/code&gt; menu loop.&lt;/p&gt;

&lt;p&gt;In the latitude/longitude input flow, I added validation with &lt;code&gt;isNaN&lt;/code&gt; and a range check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter latitude: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Latitude must be a number between -90 and 90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weather code is converted to text by looking it up in &lt;code&gt;WeatherCode&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weather_code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weatherName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WeatherCode&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;WeatherCode&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And rather than hardcoding the units, I pull them from the API response's &lt;code&gt;current_units&lt;/code&gt; (another point improved from an AI catch).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; temperature: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; wind speed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, &lt;code&gt;main().catch()&lt;/code&gt; catches any unexpected errors in one place.&lt;/p&gt;




&lt;h3&gt;
  
  
  api.test.ts — Mocking fetch
&lt;/h3&gt;

&lt;p&gt;The highlight of testing this project was &lt;strong&gt;mocking &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Success case: hit the real API and check
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;can fetch the weather for Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;35.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;139.76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;weather_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;toMatchObject&lt;/code&gt; + &lt;code&gt;expect.any(Number)&lt;/code&gt; verifies &lt;strong&gt;not just that a field exists, but that its type matches&lt;/strong&gt; — all at once. I first wrote this with &lt;code&gt;toBeDefined()&lt;/code&gt; only, but that would pass even if the type was off, so I improved it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Failure case: swap fetch for a mock
&lt;/h4&gt;

&lt;p&gt;I can't conveniently produce a broken API for the error path, so I swap &lt;code&gt;fetch&lt;/code&gt; for a mock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;throws when the API returns an error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;35.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;139.76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreAllMocks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last time, &lt;code&gt;fs&lt;/code&gt; couldn't be patched with &lt;code&gt;jest.spyOn&lt;/code&gt; and needed &lt;code&gt;jest.mock()&lt;/code&gt;, but &lt;code&gt;global.fetch&lt;/code&gt; can be swapped with &lt;code&gt;jest.spyOn(global, "fetch")&lt;/code&gt;. The lesson from last time — that which approach works depends on the target's property descriptor (&lt;code&gt;configurable&lt;/code&gt;) — carried over directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Cleanup belongs in afterEach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I first wrote &lt;code&gt;jest.restoreAllMocks()&lt;/code&gt; inside the test body, but if the test fails it never reaches that line and the mock stays in place. I moved it into &lt;code&gt;afterEach&lt;/code&gt; so cleanup runs whether the test passes or fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; PASS  src/__tests__/api.test.ts

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Time:        1.216 s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spec clarification&lt;/td&gt;
&lt;td&gt;Identified unclear and missing requirements up front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type fix&lt;/td&gt;
&lt;td&gt;Caught numeric fields inside &lt;code&gt;current&lt;/code&gt; typed as &lt;code&gt;string&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded units&lt;/td&gt;
&lt;td&gt;Suggested reading units from &lt;code&gt;current_units&lt;/code&gt; instead of fixing them to m/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weather code key mismatch&lt;/td&gt;
&lt;td&gt;Caught string key "0" not matching the numeric &lt;code&gt;weather_code&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unused import removal&lt;/td&gt;
&lt;td&gt;Removed &lt;code&gt;WeatherResponse&lt;/code&gt; that wasn't used in &lt;code&gt;index.ts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test type verification&lt;/td&gt;
&lt;td&gt;Improved from &lt;code&gt;toBeDefined()&lt;/code&gt; to &lt;code&gt;toMatchObject&lt;/code&gt; + &lt;code&gt;expect.any()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock cleanup&lt;/td&gt;
&lt;td&gt;Suggested moving &lt;code&gt;restoreAllMocks()&lt;/code&gt; into &lt;code&gt;afterEach&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peer dependency check&lt;/td&gt;
&lt;td&gt;Showed how to use &lt;code&gt;npm info&lt;/code&gt; with &lt;code&gt;peerDependencies&lt;/code&gt; to check a package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I typed numeric response fields as string
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Inside &lt;code&gt;current&lt;/code&gt; on &lt;code&gt;WeatherResponse&lt;/code&gt;, I defined &lt;code&gt;temperature_2m&lt;/code&gt; and others as &lt;code&gt;string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; Looking at the actual API response, temperature, humidity, weather code, and wind speed are all &lt;strong&gt;numbers&lt;/strong&gt;. The units (&lt;code&gt;°C&lt;/code&gt;, &lt;code&gt;%&lt;/code&gt;, etc.) live separately in &lt;code&gt;current_units&lt;/code&gt; as strings — I was conflating the two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ a number, but defined as string&lt;/span&gt;
&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ numbers are number&lt;/span&gt;
&lt;span class="nl"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;temperature_2m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Check type definitions against the real API response. Deciding "this feels string-ish" leads to a runtime mismatch — an obvious lesson I learned the hard way.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. I hardcoded the units
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I wrote the wind speed unit as &lt;code&gt;"m/s"&lt;/code&gt; directly in the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; Open-Meteo actually returns wind speed in &lt;strong&gt;&lt;code&gt;km/h&lt;/code&gt;&lt;/strong&gt;. Hardcoding it would make the display a lie. And since the API returns the units in a &lt;code&gt;current_units&lt;/code&gt; field, using that is the right answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ unit fixed in code&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`wind speed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;m/s`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ use the unit the API returns&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`wind speed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wind_speed_10m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Don't hardcode units or config values — pulling them from the data source is more resilient to spec changes. I aligned temperature and humidity to the same approach.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. The weather code keys didn't match because they were strings
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I wrote the &lt;code&gt;WeatherCode&lt;/code&gt; keys as the strings &lt;code&gt;"0"&lt;/code&gt; and &lt;code&gt;"1"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The API's &lt;code&gt;weather_code&lt;/code&gt; comes back as a &lt;strong&gt;number&lt;/strong&gt;. When looking it up with &lt;code&gt;WeatherCode[weather_code]&lt;/code&gt;, the keys need to be defined as numbers or the lookup won't work as intended.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ string keys&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WeatherCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ numeric keys (match the numeric weather_code from the API)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WeatherCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Match "the key type" to "the type you look it up with." Object keys can look similar but are different things as numbers vs. strings — a good reminder.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. AI's suggestions aren't always correct
&lt;/h3&gt;

&lt;p&gt;This was less a sticking point and more a &lt;strong&gt;turning point in how I learn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During development, AI flagged that "the combination of &lt;code&gt;jest ^30&lt;/code&gt; and &lt;code&gt;ts-jest ^29&lt;/code&gt; may have a version mismatch." Rather than taking it at face value, I checked it myself with &lt;code&gt;npm info&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm info ts-jest peerDependencies
&lt;span class="c"&gt;# result: jest: '^29.0.0 || ^30.0.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ts-jest&lt;/code&gt; supports both jest 29 and 30, so in reality &lt;strong&gt;there was no problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; AI suggestions are just hints. When I can confirm a fact with a command, I confirm it myself. I want to build the habit of "verify and then decide" rather than "fix it because AI said so."&lt;/p&gt;

&lt;p&gt;(Side note: &lt;code&gt;npm install&lt;/code&gt; brings each package to its latest version independently, so combined versions can drift. But npm installs them anyway without erroring, which makes it easy to miss — learning that I can check ahead of time by running &lt;code&gt;npm info&lt;/code&gt; against a package's &lt;code&gt;peerDependencies&lt;/code&gt; was a real gain.)&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  async/await and fetch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Writing &lt;code&gt;await fetch(url)&lt;/code&gt; inside an &lt;code&gt;async function&lt;/code&gt; lets you write async code top-to-bottom, sequentially&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch&lt;/code&gt; does not throw on HTTP errors — you check &lt;code&gt;response.ok&lt;/code&gt; yourself&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response.json()&lt;/code&gt; extracts the response body as JSON&lt;/li&gt;
&lt;li&gt;Some Java HTTP clients throw depending on the status code, so this needed a mental shift&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TypeScript Types
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response type definition&lt;/td&gt;
&lt;td&gt;You can model each JSON field with &lt;code&gt;type&lt;/code&gt;. Choosing &lt;code&gt;number&lt;/code&gt; vs &lt;code&gt;string&lt;/code&gt; matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyof typeof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extracts an existing object's keys as a type. I auto-generated a union of city names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numeric vs string keys&lt;/td&gt;
&lt;td&gt;Object keys differ as numbers vs strings. Match them to the lookup type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Jest Mocking
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest.spyOn(global, "fetch")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Swaps the global &lt;code&gt;fetch&lt;/code&gt; for a mock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mockResolvedValue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a mock that returns a Promise (for mocking async functions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rejects.toThrow()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifies that an async operation throws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;toMatchObject&lt;/code&gt; + &lt;code&gt;expect.any()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Verifies structure and type at once — stricter than &lt;code&gt;toBeDefined()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup in &lt;code&gt;afterEach&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mocks reliably reset even when a test fails. Equivalent to Java's &lt;code&gt;@AfterEach&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't hardcode units and config values&lt;/strong&gt; — pulling them from the data source is more resilient to spec changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove unused imports&lt;/strong&gt; (readability and maintainability)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests should be written with "what am I verifying" in mind, not just "does it run"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify AI's suggestions with commands&lt;/strong&gt; — don't take them at face value&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;I built this with the same style as before: write the code myself and have AI review it.&lt;/p&gt;

&lt;p&gt;Three things stood out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check type definitions against the real response:&lt;/strong&gt; Mixing up &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;number&lt;/code&gt; was a mistake the actual JSON would have prevented. "Look at the real thing, then write the type" is more reliable than "imagine the type first."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sense for avoiding hardcoding:&lt;/strong&gt; Pulling units from the API made me pause the next time I felt the urge to hardcode a value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use AI, but confirm it yourself in the end:&lt;/strong&gt; Verifying the version-mismatch flag with &lt;code&gt;npm info&lt;/code&gt; myself was my biggest gain this time. AI is convenient, but I want to keep the initiative on fact-checking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my record of building an external API tool (a weather CLI) as a TypeScript beginner.&lt;/p&gt;

&lt;p&gt;Progress since the previous projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Called an external API asynchronously with &lt;code&gt;async/await&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modeled the API response structure accurately with &lt;code&gt;type&lt;/code&gt; (choosing &lt;code&gt;number&lt;/code&gt;/&lt;code&gt;string&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used &lt;code&gt;keyof typeof&lt;/code&gt; to leverage object keys as a type&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mocked &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;jest.spyOn(global, "fetch")&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built the habit of verifying AI's suggestions myself with &lt;code&gt;npm info&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next up: a simple HTTP client that calls my own API (FastAPI), going deeper on &lt;code&gt;async/await&lt;/code&gt; and type definitions.&lt;/p&gt;

&lt;p&gt;Full learning log: &lt;a href="https://github.com/uya0526-design/weather_api/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>jest</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Self-Host Your GitHub Stats Badge on Vercel — Fixing the 'Broken Image' on Your Profile README</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Thu, 04 Jun 2026 12:42:49 +0000</pubDate>
      <link>https://dev.to/uya0526design/self-host-your-github-stats-badge-on-vercel-fixing-the-broken-image-on-your-profile-readme-la0</link>
      <guid>https://dev.to/uya0526design/self-host-your-github-stats-badge-on-vercel-fixing-the-broken-image-on-your-profile-readme-la0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🌐 This is the English version of an article I originally wrote in Japanese on Zenn. The original is linked as the canonical source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You've probably seen that "GitHub Stats" card (&lt;code&gt;github-readme-stats&lt;/code&gt;) on a lot of profile READMEs. Ever pasted it in, only to get a broken image? I have.&lt;/p&gt;

&lt;p&gt;I touched on this problem in my previous article, but the fix — self-hosting — has enough steps that I split it into its own post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The background (why it breaks in the first place) is covered in the previous article. Worth reading alongside this one.&lt;br&gt;
👉 &lt;a href="https://dev.to/uya0526design/i-built-a-github-profile-readme-the-it-wont-show-up-traps-and-tips-to-stand-out-4pba"&gt;I Built a GitHub Profile README — the 'It Won't Show Up' Traps and Tips to Stand Out&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I'll walk through how to &lt;strong&gt;self-host &lt;code&gt;github-readme-stats&lt;/code&gt; on your own Vercel account so it renders reliably&lt;/strong&gt;, along with the points that are easy to trip over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-hosting is needed
&lt;/h2&gt;

&lt;p&gt;The official instance of &lt;code&gt;github-readme-stats&lt;/code&gt; (&lt;code&gt;github-readme-stats.vercel.app&lt;/code&gt;) is shared by many users. Because of that, it easily hits the free-tier API limits, and during busy periods the image won't render (it returns &lt;code&gt;503 SERVICE_UNAVAILABLE&lt;/code&gt; / &lt;code&gt;DEPLOYMENT_PAUSED&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This isn't a problem on your side — it's a known issue that happens intermittently due to the shared service. &lt;strong&gt;If you self-host, you get your own dedicated instance&lt;/strong&gt;, so you no longer depend on someone else's quota and it renders reliably.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Assume that badges depending on external services will go down someday. For the important ones, self-hosting is the most reassuring option in the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background terms
&lt;/h2&gt;

&lt;p&gt;Before the steps, here's a quick rundown of the terms that come up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;A cloud deploy / hosting platform for frontend apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hobby plan&lt;/td&gt;
&lt;td&gt;The free plan for personal, non-commercial use. Fine for portfolio purposes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless function&lt;/td&gt;
&lt;td&gt;A function that runs only on request instead of a server running constantly. Vercel uses this to generate the image dynamically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment variable&lt;/td&gt;
&lt;td&gt;A config value passed in safely from outside instead of hard-coded. Here it holds the GitHub API token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fork&lt;/td&gt;
&lt;td&gt;Copying someone else's GitHub repository into your own account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;p&gt;The overall flow is six steps: register on Vercel → fork the repo → get a token → deploy → check the URL → embed in your README.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Vercel account
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://vercel.com/signup" rel="noopener noreferrer"&gt;https://vercel.com/signup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;"Continue with GitHub"&lt;/strong&gt; to link your GitHub account&lt;/li&gt;
&lt;li&gt;For the plan, choose &lt;strong&gt;"Hobby"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If asked for phone verification, enter it in international format (for Japan, choose &lt;code&gt;+81&lt;/code&gt; and drop the leading &lt;code&gt;0&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Gotcha: the "The user could not be found" error&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with a correctly entered phone number, you may get this error. It seems to be a known issue on Vercel's side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Submit the form at &lt;a href="https://vercel.com/accountrecovery" rel="noopener noreferrer"&gt;https://vercel.com/accountrecovery&lt;/a&gt; to contact support. Example for the "Additional Details" field:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I tried to sign up via GitHub and was asked to verify my phone number. I entered my mobile number but received the error message "The user could not be found." I have tried re-entering the number multiple times but the issue persists. This is a new account and the phone number has not been used with any other Vercel account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Fork the repository
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;https://github.com/anuraghazra/github-readme-stats&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Fork"&lt;/strong&gt; button at the top right&lt;/li&gt;
&lt;li&gt;Confirm it's been forked into your own GitHub account&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Get a GitHub Personal Access Token
&lt;/h3&gt;

&lt;p&gt;This is needed so &lt;code&gt;github-readme-stats&lt;/code&gt; can call the GitHub API.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;https://github.com/settings/tokens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Generate new token" → "Generate new token (classic)"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Settings:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Note&lt;/strong&gt;: anything (e.g. &lt;code&gt;vercel-readme-stats&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration&lt;/strong&gt;: &lt;code&gt;No expiration&lt;/code&gt; (or any period you like)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select scopes&lt;/strong&gt;: check &lt;code&gt;public_repo&lt;/code&gt; (use &lt;code&gt;repo&lt;/code&gt; if you also want to count private repositories)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Generate token"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy and save&lt;/strong&gt; the token shown (the string starting with &lt;code&gt;ghp_&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security note:&lt;/strong&gt; The token is only shown once — once you close this screen it's gone, so be sure to save it. Also, never write the token directly in your code or README; pass it only as a Vercel environment variable in the next step, and don't leak it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Deploy to Vercel
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://vercel.com/new" rel="noopener noreferrer"&gt;https://vercel.com/new&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;From the Import Git Repository list, select &lt;code&gt;github-readme-stats&lt;/code&gt; and click &lt;strong&gt;"Import"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;"Environment Variables"&lt;/strong&gt; section and add the following:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;| Key | Value |&lt;br&gt;
   | :--- | :--- |&lt;br&gt;
   | &lt;code&gt;PAT_1&lt;/code&gt; | The token from step 3 (the string starting with &lt;code&gt;ghp_&lt;/code&gt;) |&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave the other settings at their defaults and click &lt;strong&gt;"Deploy"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When the "Congratulations!" screen appears, the deployment is done&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  5. Check the deploy URL
&lt;/h3&gt;

&lt;p&gt;Back on the dashboard, two kinds of URL are shown. &lt;strong&gt;The one you use is the fixed URL&lt;/strong&gt;, so be careful not to mix them up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The URL in the Domains field (e.g. &lt;code&gt;github-readme-stats-xxx-yyy.vercel.app&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The fixed production URL. Use this one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The long URL in the Deployment field (e.g. &lt;code&gt;github-readme-stats-aabbccddee-xxx.vercel.app&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A temporary URL that changes per deploy. Don't use it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To verify, open the following in a browser; if the Stats card appears, you're good.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://[your Domains URL]/api?username=[your GitHub username]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Embed it in your README
&lt;/h3&gt;

&lt;p&gt;Add the following to your profile &lt;code&gt;README.md&lt;/code&gt;, using your own Domains URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;GitHub Stats&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://[your&lt;/span&gt; Domains URL]/api?username=[username]&amp;amp;show_icons=true&amp;amp;hide_border=true&amp;amp;count_private=true)
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Top Languages&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://[your&lt;/span&gt; Domains URL]/api/top-langs/?username=[username]&amp;amp;layout=compact&amp;amp;hide_border=true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the Stats card renders from your own dedicated instance, unaffected by the congestion on the official one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common customization options
&lt;/h2&gt;

&lt;p&gt;You can tweak the look and what's counted via URL query parameters. Here are the ones I use most.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;theme=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Theme color&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;radical&lt;/code&gt;, &lt;code&gt;dark&lt;/code&gt;, &lt;code&gt;tokyonight&lt;/code&gt;, &lt;code&gt;merko&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;show_icons=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show icons&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;hide_border=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hide the border&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;count_private=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Also count private repositories&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;hide=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hide specific items&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;hide=issues,contribs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;layout=compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compact layout for the language card&lt;/td&gt;
&lt;td&gt;for top-langs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hobby plan limits and caveats
&lt;/h2&gt;

&lt;p&gt;For this use case (just showing it on your own README), traffic is extremely low, so the Hobby plan is plenty. Still, keep these in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serverless function execution limit: 10 seconds&lt;/strong&gt; (it could rarely time out if the GitHub API is slow to respond, but in practice it's almost never an issue)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth: 100 GB/month&lt;/strong&gt; (nowhere near reachable for personal use)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No commercial use&lt;/strong&gt; (ads, paid services, etc. require the Pro plan)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The broken-image problem with &lt;code&gt;github-readme-stats&lt;/code&gt; is usually caused by "the official instance being paused" — not a mistake on your side. Even so, putting something that might go down at any time on the face of your profile is unsettling, so I recommend self-hosting to get your own dedicated instance.&lt;/p&gt;

&lt;p&gt;To recap the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register on Vercel (Hobby plan is enough)&lt;/li&gt;
&lt;li&gt;Fork &lt;code&gt;github-readme-stats&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Get a GitHub Personal Access Token (&lt;code&gt;public_repo&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Put the token in the &lt;code&gt;PAT_1&lt;/code&gt; environment variable and Deploy&lt;/li&gt;
&lt;li&gt;Note the fixed Domains URL&lt;/li&gt;
&lt;li&gt;Swap your README's image URL for your own Domains URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Six steps in total. Once you've done it, it just keeps working reliably.&lt;/p&gt;

&lt;p&gt;You can see my card in action on my profile 👉 &lt;a href="https://github.com/uya0526-design" rel="noopener noreferrer"&gt;github.com/uya0526-design&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;anuraghazra/github-readme-stats&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel pricing (Hobby / Pro)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/accountrecovery" rel="noopener noreferrer"&gt;Vercel Account Recovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;GitHub Personal Access Tokens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vercel</category>
      <category>github</category>
      <category>readme</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built a GitHub Profile README — the 'It Won't Show Up' Traps and Tips to Stand Out</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Thu, 04 Jun 2026 12:21:49 +0000</pubDate>
      <link>https://dev.to/uya0526design/i-built-a-github-profile-readme-the-it-wont-show-up-traps-and-tips-to-stand-out-4pba</link>
      <guid>https://dev.to/uya0526design/i-built-a-github-profile-readme-the-it-wont-show-up-traps-and-tips-to-stand-out-4pba</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🌐 This is the English version of an article I originally wrote in Japanese on Zenn. The original is linked as the canonical source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I usually write "I built a small CLI tool in TypeScript / Python" style posts. This time it's a bit different: a write-up about building my &lt;strong&gt;GitHub profile README&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'm an engineer who spent 15+ years mostly on legacy Java, and I'm now learning TypeScript and Python on my own, publishing what I learn on Zenn and dev.to. Once I had a decent number of learning projects piling up, I decided it was time to build "the face of my GitHub."&lt;/p&gt;

&lt;p&gt;Here's the conclusion up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing the README itself is not hard.&lt;/strong&gt; You just line up Markdown and badges.&lt;/li&gt;
&lt;li&gt;But &lt;strong&gt;I kept getting stuck on "it doesn't show up on my profile."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;And there was a second trap: &lt;strong&gt;the GitHub Stats badge was broken and wouldn't render.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post focuses on the points that are surprisingly easy to miss if you only read the official docs, using my own profile as a concrete example.&lt;/p&gt;

&lt;p&gt;Here's the finished result 👉 &lt;a href="https://github.com/uya0526-design" rel="noopener noreferrer"&gt;github.com/uya0526-design&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 I'm writing this for people building a profile README for the first time. If you've written repository READMEs before but never the profile one, this should be about the right level.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is a profile README, anyway?
&lt;/h2&gt;

&lt;p&gt;GitHub has two kinds of README, and it's easy to confuse them at first. Let me separate them clearly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;th&gt;Where it shows up&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository README&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;README.md&lt;/code&gt; inside each repo&lt;/td&gt;
&lt;td&gt;Top of that repository&lt;/td&gt;
&lt;td&gt;Explaining the project and how to use it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profile README&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;README.md&lt;/code&gt; inside a repo &lt;strong&gt;named exactly like your username&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Top of your profile page (&lt;code&gt;github.com/username&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Self-introduction and the "face" of your portfolio&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point: the profile README differs from the repository README you normally write, both in where it lives and what it's for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The essentials of making one
&lt;/h3&gt;

&lt;p&gt;The mechanism is simple. You create a repository whose name &lt;strong&gt;exactly matches your username&lt;/strong&gt;, and put a &lt;code&gt;README.md&lt;/code&gt; at its root.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username &lt;code&gt;uya0526-design&lt;/code&gt; → repository &lt;code&gt;uya0526-design/uya0526-design&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub treats this repo as a "special repository"&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;README.md&lt;/code&gt; at the root&lt;/li&gt;
&lt;li&gt;Make the repository &lt;strong&gt;Public&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In theory it should now show up... and this is where I fell into the swamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: the README doesn't show up
&lt;/h2&gt;

&lt;p&gt;I wrote the README, pushed it, and nothing appeared on my profile. I burned a fair amount of time in this state. Let me share the final cause and a checklist for narrowing it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real cause: I hadn't pressed the "Share to profile" button
&lt;/h3&gt;

&lt;p&gt;This was the actual culprit in my case.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Just creating the special repository does not always auto-publish it to your profile&lt;/li&gt;
&lt;li&gt;There's a &lt;strong&gt;"Share to profile" button&lt;/strong&gt; on the repository page, and only after pressing it does the README appear on your profile&lt;/li&gt;
&lt;li&gt;The button is subtle, and I completely missed it&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;Takeaway:&lt;/strong&gt; When your README won't show up even after you wrote and pushed it, first look for the "Share to profile" button on the repository page. In my case, this one action got me out of the swamp.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Checklist for when it won't show up
&lt;/h3&gt;

&lt;p&gt;That said, this isn't the only possible cause. There are several easy-to-miss points. Checking them top to bottom is efficient.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the repository name match your username exactly?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Watch out for mixing up hyphen &lt;code&gt;-&lt;/code&gt; and underscore &lt;code&gt;_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Match case as well&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the repository Public?&lt;/strong&gt; (Private repos won't show up)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is &lt;code&gt;README.md&lt;/code&gt; at the root?&lt;/strong&gt; (It won't be picked up from a subfolder)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the README have content?&lt;/strong&gt; (An empty file won't show)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the README at the root of the default branch?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The profile README only references the README on the "default branch"&lt;/li&gt;
&lt;li&gt;If you created it locally on &lt;code&gt;master&lt;/code&gt;, pushed, and added &lt;code&gt;main&lt;/code&gt; afterward, the default branch and the branch holding the README can end up out of sync&lt;/li&gt;
&lt;li&gt;Check under &lt;code&gt;Settings → General → Default branch&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did you press "Share to profile"?&lt;/strong&gt; ← my actual cause&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propagation delay / cache&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;There can be a lag right after pushing. Wait a bit, or hard reload (&lt;code&gt;Ctrl + Shift + R&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Number 5 is an especially easy trap for people who used &lt;code&gt;master&lt;/code&gt; by local habit. Maybe a classic "ex-SE" mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  A tip for checking: view it while logged out
&lt;/h3&gt;

&lt;p&gt;This one helped in a quiet but real way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open &lt;code&gt;github.com/username&lt;/code&gt; in &lt;strong&gt;incognito / InPrivate mode&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When you're logged in, the editing UI takes priority and the appearance can differ, so checking "how others see it" while logged out is the reliable way&lt;/li&gt;
&lt;li&gt;When it's correctly reflected, the README appears large, &lt;strong&gt;above your Pinned repositories&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;In other words, if Pinned is at the very top, the README hasn't been reflected yet&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trap 2: the GitHub Stats badge is broken
&lt;/h2&gt;

&lt;p&gt;You know that "GitHub Stats" card (&lt;code&gt;github-readme-stats&lt;/code&gt;) you often see on profile READMEs? When I pasted it in, the image turned into a broken-image icon.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cause: the official instance was paused
&lt;/h3&gt;

&lt;p&gt;When I opened the image URL directly in a browser, it returned &lt;code&gt;503 SERVICE_UNAVAILABLE&lt;/code&gt; / &lt;code&gt;DEPLOYMENT_PAUSED&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This means the official deployment (on a free Vercel tier) is temporarily paused due to congestion or limits&lt;/li&gt;
&lt;li&gt;Since it's a service shared by many users, this is a known issue that happens intermittently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It's not a problem on your side&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, my README was fine — the shared service I was riding on happened to be down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: self-host on your own Vercel
&lt;/h3&gt;

&lt;p&gt;If it's a temporary outage you can just wait, but I wasn't comfortable putting something that might go down again on the "face" of my profile. So I decided to &lt;strong&gt;self-host on my own Vercel account&lt;/strong&gt;. That removes the dependency on someone else's quota.&lt;/p&gt;

&lt;p&gt;Here are just the key steps (the &lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;official repository&lt;/a&gt; README has the details).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Vercel account (the free &lt;strong&gt;Hobby plan&lt;/strong&gt; is enough for personal use)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork&lt;/strong&gt; &lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;github-readme-stats&lt;/a&gt; to your own account&lt;/li&gt;
&lt;li&gt;Get a GitHub &lt;strong&gt;Personal Access Token&lt;/strong&gt; (classic, with the &lt;code&gt;public_repo&lt;/code&gt; scope)&lt;/li&gt;
&lt;li&gt;Import it into Vercel, set the token as the environment variable &lt;code&gt;PAT_1&lt;/code&gt;, and &lt;strong&gt;Deploy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;fixed Domains URL&lt;/strong&gt; that gets issued (not the temporary URL that changes per deploy)&lt;/li&gt;
&lt;li&gt;Replace the image URL in your README, swapping the official instance for your own Domains URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After swapping, the embed looks like this (replace the URL part with your own):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;GitHub Stats&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://[your&lt;/span&gt; Domains URL]/api?username=[username]&amp;amp;show_icons=true&amp;amp;hide_border=true&amp;amp;count_private=true)
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Top Languages&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://[your&lt;/span&gt; Domains URL]/api/top-langs/?username=[username]&amp;amp;layout=compact&amp;amp;hide_border=true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security note:&lt;/strong&gt; A Personal Access Token is shown only once — copy it before you close the screen. Never write the token directly in your code or README; pass it only as a Vercel environment variable, and don't leak it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;If you get stuck signing up for Vercel (phone verification error):&lt;/strong&gt; When signing up via GitHub, phone verification can fail with "The user could not be found" (this seems to be a known Vercel issue). If that happens, use the form at &lt;a href="https://vercel.com/accountrecovery" rel="noopener noreferrer"&gt;Vercel Account Recovery&lt;/a&gt; to explain the situation and contact support.&lt;/p&gt;

&lt;p&gt;You can see my self-hosted card working in the "GitHub Stats" section near the bottom of my profile.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📝 This self-hosting procedure turned out to be enough material for its own article. I plan to split out the detailed steps (with screenshots) into a separate post.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; Assume that badges depending on external services will go down someday. Self-host the important ones, or place them where it isn't fatal if they break.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to highlight: from a reskilling ex-engineer's view
&lt;/h2&gt;

&lt;p&gt;Beyond just "making a working README," the real question is &lt;strong&gt;what you communicate to the reader&lt;/strong&gt; (recruiters, collaborators, peers). My position is "15+ years of real experience, but modern tech is still ahead of me," so I focused on showing that honestly and attractively.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I included
&lt;/h3&gt;

&lt;p&gt;In my actual profile, I structured it roughly in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header (one-line catch + badges):&lt;/strong&gt; years of experience, key certifications, and what I'm learning, at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About Me:&lt;/strong&gt; which domains I've worked in and for how long, plus where I am now (reskilling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How I Learn:&lt;/strong&gt; less abstract self-PR, more "how I actually move my hands"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certifications:&lt;/strong&gt; a table with dates (high credibility, easy to read)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tech Stack:&lt;/strong&gt; badges &lt;strong&gt;split&lt;/strong&gt; into "professional experience" and "currently learning"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Featured Projects:&lt;/strong&gt; link + tech used + what I practiced, as a set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing:&lt;/strong&gt; links to Zenn / dev.to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Stats:&lt;/strong&gt; the self-hosted version above&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ways of thinking that work for self-promotion
&lt;/h3&gt;

&lt;p&gt;Four things I paid special attention to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concrete &amp;gt; abstract.&lt;/strong&gt;
"Lots of Java experience" is weaker than "15 years of enterprise Java in telecom, logistics, and finance." Specific industry names add credibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate real experience from what you're learning, honestly.&lt;/strong&gt;
Categorizing accurately without exaggeration actually builds trust. That's why I split my Tech Stack into "Professional experience" and "Currently learning &amp;amp; building."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show how you learn.&lt;/strong&gt;
In my case I wrote about the process itself: "I don't let AI write my code — I implement first, then have AI review," "I design my own tests," "I don't take AI's suggestions at face value — I verify them with actual commands." &lt;strong&gt;Showing the process is itself a statement about your attitude as an engineer.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack up "finished and published" wins, however small.&lt;/strong&gt;
A pile of small projects with tests, a README, and a learning log is plain but strong.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A small trick for going bilingual
&lt;/h3&gt;

&lt;p&gt;Assuming some global readers, I wrote mainly in English while folding a Japanese version into a collapsible block. That keeps an English-first layout while still being readable for Japanese speakers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Japanese profile (click to expand)&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;

(Japanese body goes here)

&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 GitHub READMEs support some HTML (like collapsible blocks and centered text), but behavior can vary by environment. Always check the actual rendering after publishing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wrap-up: a checklist for next time
&lt;/h2&gt;

&lt;p&gt;Finally, a checklist for my future self (and for you, if you read this far).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Created a repository with the &lt;strong&gt;same name&lt;/strong&gt; as your username (exact match)&lt;/li&gt;
&lt;li&gt;[ ] Made it &lt;strong&gt;Public&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Put &lt;code&gt;README.md&lt;/code&gt; at the root of the &lt;strong&gt;default branch&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Pressed the "Share to profile" button&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Opened &lt;code&gt;github.com/username&lt;/code&gt; in incognito mode to verify&lt;/li&gt;
&lt;li&gt;[ ] Checked that external badges (Stats, etc.) render (self-host if not)&lt;/li&gt;
&lt;li&gt;[ ] Wrote "real experience" and "currently learning" separately and accurately&lt;/li&gt;
&lt;li&gt;[ ] Made the content convey your learning process and consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A profile README isn't something you make once and forget — it's a place to keep showing your growth. I'll keep updating Featured Projects and "Next up" as projects accumulate.&lt;/p&gt;

&lt;p&gt;Here's the finished profile 👉 &lt;a href="https://github.com/uya0526-design" rel="noopener noreferrer"&gt;github.com/uya0526-design&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope it helps anyone else stuck in the "it won't show up" swamp.&lt;/p&gt;

</description>
      <category>github</category>
      <category>readme</category>
      <category>vercel</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Quiz CLI with TypeScript — enum, Tuple Types, and Jest Mocks</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:34:49 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-quiz-cli-with-typescript-enum-tuple-types-and-jest-mocks-30kl</link>
      <guid>https://dev.to/uya0526design/building-a-quiz-cli-with-typescript-enum-tuple-types-and-jest-mocks-30kl</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my fifth article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my previous article, I built a Household Budget CLI and learned about &lt;code&gt;class&lt;/code&gt;, generics &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;interface extends&lt;/code&gt;, and array methods like &lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, and &lt;code&gt;find&lt;/code&gt;. This time, I built a &lt;strong&gt;Quiz CLI&lt;/strong&gt; and focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managing choice IDs safely with &lt;strong&gt;&lt;code&gt;enum&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Constraining array length at the type level with &lt;strong&gt;tuple types&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Replacing callback hell with &lt;strong&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Swapping out the &lt;code&gt;fs&lt;/code&gt; module entirely with &lt;strong&gt;&lt;code&gt;jest.mock()&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Understanding when to use &lt;strong&gt;&lt;code&gt;jest.spyOn&lt;/code&gt;&lt;/strong&gt; vs &lt;strong&gt;&lt;code&gt;jest.mock()&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as always — I write honestly about where I got stuck, what I thought through, and what I asked AI for.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;My rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI tool to create, edit, delete, and randomly play quiz questions stored in a JSON file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;==============================
1. Add a quiz
2. Edit a quiz
3. Delete a quiz
4. Play quizzes (3 random questions)
5. Show all quizzes
6. Exit
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1
&lt;span class="go"&gt;Enter the question: What keyword defines a type alias in TypeScript?
Enter option 1: type
Enter option 2: interface
Enter the correct answer (1 or 2): 1
Quiz added.

&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;4
&lt;span class="go"&gt;--- Question 1 ---
What keyword defines a type alias in TypeScript?
1: type
2: interface
Your answer (1 or 2): 1
✅ Correct!

Result: 1 / 1 correct
==============================
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Quiz data is saved to a JSON file and persists between sessions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/simple_quiz" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/simple_quiz&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;simple_quiz/
├── src/
│   ├── index.ts             # Entry point / CLI menu
│   ├── quiz.ts              # Business logic (QuizManager class)
│   ├── types.ts             # Type definitions
│   └── __tests__/
│       └── quiz.test.ts     # Unit tests (10 cases)
├── quizzes.json             # Persistent quiz data
├── jest.config.js
├── tsconfig.json
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three-layer structure as the previous projects: &lt;code&gt;types.ts&lt;/code&gt; (types) / &lt;code&gt;quiz.ts&lt;/code&gt; (logic) / &lt;code&gt;index.ts&lt;/code&gt; (UI).&lt;br&gt;
New this time: &lt;strong&gt;JSON file persistence&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; and &lt;code&gt;fs&lt;/code&gt; modules)&lt;/li&gt;
&lt;li&gt;Jest + ts-jest (unit testing)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;
&lt;h3&gt;
  
  
  types.ts — Type Definitions
&lt;/h3&gt;

&lt;p&gt;Two key ideas drove the type design this time: &lt;strong&gt;&lt;code&gt;enum&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;tuple types&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  enum to name choice IDs
&lt;/h4&gt;

&lt;p&gt;The quiz is always two choices. My first instinct was to use plain &lt;code&gt;number&lt;/code&gt;, but &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; on their own carry no meaning. &lt;code&gt;enum&lt;/code&gt; lets me give those numbers names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;OptionId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;One&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Two&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OptionId.One&lt;/code&gt; is immediately readable — you know it means "the first option." Same concept as a Java &lt;code&gt;enum&lt;/code&gt; paired with an integer value.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tuple types to fix array length
&lt;/h4&gt;

&lt;p&gt;I first typed the options array as &lt;code&gt;option[]&lt;/code&gt;. The problem: that accepts zero items, three items, or any count — but a quiz always has exactly two choices. A tuple locks that down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OptionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Quiz&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// exactly 2 — enforced at compile time&lt;/span&gt;
  &lt;span class="nl"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OptionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;[option, option]&lt;/code&gt;, TypeScript rejects anything that isn't exactly two elements. &lt;code&gt;option[]&lt;/code&gt; couldn't prevent that — tuple types can.&lt;/p&gt;




&lt;h3&gt;
  
  
  quiz.ts — The QuizManager Class
&lt;/h3&gt;

&lt;p&gt;I decided to separate logic into a &lt;code&gt;QuizManager&lt;/code&gt; class and keep &lt;code&gt;index.ts&lt;/code&gt; as pure I/O. The previous household budget project taught me that this separation makes testing much easier — same principle as a Java Service layer vs. Controller layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuizManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Quiz&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;loadQuizzes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILE_PATH&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;saveQuizzes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ID generation with reduce
&lt;/h4&gt;

&lt;p&gt;Rather than keeping a &lt;code&gt;nextId&lt;/code&gt; field in the stored data (which would pollute the JSON), I compute it from existing records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;getNextQuizId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;maxId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start at &lt;code&gt;0&lt;/code&gt;, walk through all IDs, take the max, add one. If a quiz is deleted and a new one is added, there are no ID collisions.&lt;/p&gt;

&lt;h4&gt;
  
  
  isQuizExists with some
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;isQuizExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guard method used before editing or deleting. &lt;code&gt;some&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if at least one element matches — Java's &lt;code&gt;Stream.anyMatch()&lt;/code&gt;. I'd used this pattern in the budget project too, so it came naturally.&lt;/p&gt;




&lt;h3&gt;
  
  
  index.ts — CLI Menu
&lt;/h3&gt;

&lt;h4&gt;
  
  
  async/await to escape callback nesting
&lt;/h4&gt;

&lt;p&gt;The budget project's &lt;code&gt;readline&lt;/code&gt; callbacks were readable, but I could already see how deeper nesting would make them hard to follow. This time I wrapped &lt;code&gt;readline&lt;/code&gt; in a &lt;code&gt;Promise&lt;/code&gt; from the start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the main flow reads top-to-bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Callback style (nesting grows with each input)&lt;/span&gt;
&lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Question: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Option 1: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Option 2: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// async/await (reads like sequential code)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter the question: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;option1&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter option 1: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;option2&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter option 2: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels similar to waiting on a Java &lt;code&gt;Future&lt;/code&gt; with &lt;code&gt;.get()&lt;/code&gt;, but without blocking a thread.&lt;/p&gt;

&lt;h4&gt;
  
  
  Random question selection
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomQuizzes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;quizManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQuizzes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort(() =&amp;gt; Math.random() - 0.5)&lt;/code&gt; is a standard shuffle idiom — the comparator returns a random positive or negative value, randomizing the sort order. &lt;code&gt;.slice(0, 3)&lt;/code&gt; then takes the first three.&lt;/p&gt;




&lt;h3&gt;
  
  
  quiz.test.ts — Unit Tests with Jest
&lt;/h3&gt;

&lt;p&gt;The toughest part of testing this project was &lt;strong&gt;mocking the &lt;code&gt;fs&lt;/code&gt; module&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  jest.spyOn vs jest.mock()
&lt;/h4&gt;

&lt;p&gt;I started with &lt;code&gt;jest.spyOn(fs, 'readFileSync')&lt;/code&gt;. Immediate error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot redefine property: readFileSync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI explained why: Node's &lt;code&gt;fs&lt;/code&gt; module methods have &lt;code&gt;configurable: false&lt;/code&gt; in their property descriptors. &lt;code&gt;jest.spyOn&lt;/code&gt; works by redefining the property — which is forbidden when &lt;code&gt;configurable&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is to replace the entire module instead of patching individual methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ fs methods are configurable: false — spyOn can't redefine them&lt;/span&gt;
&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readFileSync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ jest.mock() replaces the whole module — no configurable restriction&lt;/span&gt;
&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockedFs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;jest.spyOn&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;jest.mock()&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;One method&lt;/td&gt;
&lt;td&gt;Entire module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requirement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;configurable: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restoreAllMocks()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;resetAllMocks()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java equivalent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mockito.spy(realObject)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mockito.mock(ClassName.class)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note: &lt;code&gt;console.error&lt;/code&gt; &lt;em&gt;does&lt;/em&gt; work with &lt;code&gt;jest.spyOn&lt;/code&gt; because &lt;code&gt;console&lt;/code&gt; methods are &lt;code&gt;configurable: true&lt;/code&gt;. Whether &lt;code&gt;spyOn&lt;/code&gt; is usable depends entirely on how the object's properties are defined — not on which module it is.&lt;/p&gt;

&lt;h4&gt;
  
  
  The "accidentally passing test" trap
&lt;/h4&gt;

&lt;p&gt;While writing the &lt;code&gt;loadQuizzes&lt;/code&gt; test, I used &lt;code&gt;JSON.stringify([])&lt;/code&gt; in my assertion. It passed. AI flagged it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Accidentally passes — empty arrays produce "[]" either way&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockedFs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quizzes.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Matches the actual call exactly&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockedFs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quizzes.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JSON.stringify([])&lt;/code&gt; and &lt;code&gt;JSON.stringify([], null, 2)&lt;/code&gt; both return &lt;code&gt;"[]"&lt;/code&gt; for an empty array. Once data is present, the second form adds newlines and indentation — and the test would fail. "Test passes" does not mean "test is correct."&lt;/p&gt;

&lt;h4&gt;
  
  
  expect.stringContaining() for cross-platform paths
&lt;/h4&gt;

&lt;p&gt;File paths look different on Windows (&lt;code&gt;\&lt;/code&gt;) vs Mac/Linux (&lt;code&gt;/&lt;/code&gt;). Rather than hardcoding a full path, I match on the filename only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockedFs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quizzes.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; PASS  src/__tests__/quiz.test.ts
  loadQuizzes
    ✓ creates a new file if none exists
    ✓ loads data from an existing file
  saveQuizzes
    ✓ can save quizzes
  addQuiz
    ✓ can add a quiz
    ✓ second quiz gets ID 2
  editQuiz
    ✓ can edit a quiz
  deleteQuiz
    ✓ can delete a quiz
  isQuizExists
    ✓ returns true when the quiz exists
    ✓ returns false when the quiz does not exist
  getNextQuizId (tested indirectly via addQuiz)
    ✓ no ID collision after delete then add

Tests: 10 passed, 10 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spec clarification&lt;/td&gt;
&lt;td&gt;Identified unclear and missing requirements up front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;async/await explanation&lt;/td&gt;
&lt;td&gt;Compared it to callback style and to Java's Future&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuple type hint&lt;/td&gt;
&lt;td&gt;Suggested changing &lt;code&gt;option[]&lt;/code&gt; to &lt;code&gt;[option, option]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constructor issue&lt;/td&gt;
&lt;td&gt;Caught that &lt;code&gt;private constructor&lt;/code&gt; prevents external instantiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;editQuiz bug&lt;/td&gt;
&lt;td&gt;Pointed out &lt;code&gt;map(q =&amp;gt; q.id === id ? q : q)&lt;/code&gt; does nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest error fix&lt;/td&gt;
&lt;td&gt;Explained &lt;code&gt;Cannot redefine property&lt;/code&gt; and suggested &lt;code&gt;jest.mock()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;console.error mock&lt;/td&gt;
&lt;td&gt;Showed how to use &lt;code&gt;jest.spyOn(console, 'error')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path comparison&lt;/td&gt;
&lt;td&gt;Suggested &lt;code&gt;expect.stringContaining()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;README corrections&lt;/td&gt;
&lt;td&gt;Fixed title and tech stack description&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;private constructor&lt;/code&gt; prevented instantiation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Wrote &lt;code&gt;private constructor()&lt;/code&gt; on &lt;code&gt;QuizManager&lt;/code&gt;. Then &lt;code&gt;new QuizManager()&lt;/code&gt; threw an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;private constructor&lt;/code&gt; is intentional — it's the pattern for preventing external instantiation (e.g., singletons). A regular class should just use &lt;code&gt;constructor()&lt;/code&gt; (public by default).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Can't be instantiated from outside the class&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuizManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Public constructor — the normal case&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuizManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rule applies in Java. A slip of habit.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. editQuiz had a no-op map
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; After calling &lt;code&gt;editQuiz&lt;/code&gt;, nothing changed. AI asked me to re-read the &lt;code&gt;map&lt;/code&gt; callback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; Both branches of the ternary returned the same value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Both branches return `quiz` — nothing is updated&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Return the updated object when IDs match&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quizzes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;updatedQuiz&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent was clear — the execution was wrong. The kind of bug that's invisible until you slow down and re-read the line.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. let inside a switch case caused a scope error
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; &lt;code&gt;let score = 0&lt;/code&gt; inside &lt;code&gt;case '6'&lt;/code&gt; caused a TypeScript error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;switch&lt;/code&gt; cases share a single scope. &lt;code&gt;let&lt;/code&gt; and &lt;code&gt;const&lt;/code&gt; declarations in one case can conflict with those in others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ All cases share one scope — let can conflict across cases&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Wrap in braces to create a block scope&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapping in &lt;code&gt;{}&lt;/code&gt; gives each case its own block scope. Simple fix once you know the cause.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. console.error needed spyOn, not jest.mock()
&lt;/h3&gt;

&lt;p&gt;Unlike &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;console.error&lt;/code&gt; works fine with &lt;code&gt;jest.spyOn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockImplementation&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;console&lt;/code&gt; properties are &lt;code&gt;configurable: true&lt;/code&gt;. The lesson isn't "use &lt;code&gt;jest.mock()&lt;/code&gt; for everything" — it's "know your target's property descriptor before choosing your approach."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  async/await
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wrapping &lt;code&gt;readline&lt;/code&gt; in &lt;code&gt;new Promise((resolve) =&amp;gt; { ... })&lt;/code&gt; makes it &lt;code&gt;await&lt;/code&gt;-able&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;async/await&lt;/code&gt; turns nested callbacks into sequential, top-to-bottom code&lt;/li&gt;
&lt;li&gt;Feels like Java's &lt;code&gt;CompletableFuture.get()&lt;/code&gt;, but non-blocking&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TypeScript Types
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gives numeric values meaningful names. &lt;code&gt;OptionId.One&lt;/code&gt; beats a raw &lt;code&gt;1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuple &lt;code&gt;[A, B]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Enforces exact array length and element types at compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;export&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Any type or class used across files needs &lt;code&gt;export&lt;/code&gt; — easy to forget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Jest Mocking
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest.spyOn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works when &lt;code&gt;configurable: true&lt;/code&gt; — patches a single method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest.mock()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces the entire module — no &lt;code&gt;configurable&lt;/code&gt; restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest.mocked(fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Utility that tells TypeScript a function is already mocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest.resetAllMocks()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clears mock implementations and call history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Test Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A test that passes is not necessarily a correct test&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON.stringify([])&lt;/code&gt; and &lt;code&gt;JSON.stringify([], null, 2)&lt;/code&gt; look different but both return &lt;code&gt;"[]"&lt;/code&gt; for empty arrays&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expect.stringContaining()&lt;/code&gt; handles OS path differences without hardcoding separators&lt;/li&gt;
&lt;li&gt;Write tests for error paths (&lt;code&gt;catch&lt;/code&gt; blocks), not just happy paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Separating logic (&lt;code&gt;quiz.ts&lt;/code&gt;) from I/O (&lt;code&gt;index.ts&lt;/code&gt;) makes the logic independently testable&lt;/li&gt;
&lt;li&gt;Code that touches the filesystem during tests should be mocked out&lt;/li&gt;
&lt;li&gt;Same principle as Java's Service / Controller separation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;async/await built on last project's frustration:&lt;/strong&gt; The budget project's &lt;code&gt;readline&lt;/code&gt; callbacks were fine, but I could see the wall coming. Starting this project with &lt;code&gt;async/await&lt;/code&gt; from day one felt like applying a lesson before being forced to — that's a good sign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tuple types clicked because I had a concrete need:&lt;/strong&gt; "This quiz must always have exactly two options" is a real constraint. Expressing that constraint as &lt;code&gt;[option, option]&lt;/code&gt; instead of &lt;code&gt;option[]&lt;/code&gt; felt like TypeScript doing what it's supposed to do — enforcing things so I don't have to remember them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding &lt;code&gt;configurable&lt;/code&gt; made jest.mock() make sense:&lt;/strong&gt; If I'd just followed "use jest.mock() for fs" as a rule, I wouldn't have known why — or when to use &lt;code&gt;spyOn&lt;/code&gt; instead. Now I know the underlying reason, and I can apply the right tool in future situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Accidentally passing" tests are dangerous:&lt;/strong&gt; A test that passes for the wrong reason gives false confidence. After this experience, I now double-check: does this test &lt;em&gt;actually&lt;/em&gt; verify the behavior I care about, or did I get lucky?&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my fifth TypeScript project — a Quiz CLI with CRUD and random playback.&lt;/p&gt;

&lt;p&gt;Progress since the Household Budget:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Used &lt;code&gt;async/await&lt;/code&gt; to eliminate callback nesting in readline&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrained array length with tuple types — &lt;code&gt;[option, option]&lt;/code&gt; instead of &lt;code&gt;option[]&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gave numeric IDs meaningful names with &lt;code&gt;enum&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understood the &lt;code&gt;configurable&lt;/code&gt; property descriptor and chose &lt;code&gt;jest.mock()&lt;/code&gt; over &lt;code&gt;jest.spyOn&lt;/code&gt; for &lt;code&gt;fs&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discovered the "accidentally passing test" trap and learned to verify test correctness, not just test passage&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next up: an external API tool (weather API) to go deeper on &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;async/await&lt;/code&gt;, and response type definitions.&lt;/p&gt;

&lt;p&gt;Full learning log: &lt;a href="https://github.com/uya0526-design/simple_quiz/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>jest</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Household Budget CLI with TypeScript — class, Generics, and Jest</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Sat, 30 May 2026 13:50:35 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-household-budget-cli-with-typescript-class-generics-and-jest-474c</link>
      <guid>https://dev.to/uya0526design/building-a-household-budget-cli-with-typescript-class-generics-and-jest-474c</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my fourth article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my previous article, I built a Todo List CLI and learned about &lt;code&gt;interface&lt;/code&gt;, array methods, immutable patterns, and Jest's &lt;code&gt;beforeEach&lt;/code&gt;. This time, I built a &lt;strong&gt;Household Budget CLI&lt;/strong&gt; and focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organizing logic with a &lt;strong&gt;class&lt;/strong&gt; (closer to how I'd write Java)&lt;/li&gt;
&lt;li&gt;Designing a shared interface with &lt;strong&gt;generics &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Reducing duplication with &lt;strong&gt;&lt;code&gt;interface&lt;/code&gt; inheritance (&lt;code&gt;extends&lt;/code&gt;)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;&lt;code&gt;reduce&lt;/code&gt; / &lt;code&gt;filter&lt;/code&gt; / &lt;code&gt;some&lt;/code&gt; / &lt;code&gt;find&lt;/code&gt;&lt;/strong&gt; in real logic&lt;/li&gt;
&lt;li&gt;Writing &lt;strong&gt;date and category validation&lt;/strong&gt; from scratch&lt;/li&gt;
&lt;li&gt;Testing &lt;strong&gt;class methods&lt;/strong&gt; with Jest (13 test cases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as always — I write honestly about where I got stuck, what I thought through, and what I asked AI for.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;My rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI tool to record, edit, delete, and summarize household income and expenses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;================================================
1. Add income
2. Add expense
3. Show all records
4. Show balance summary
5. Edit income
6. Edit expense
7. Delete a record
8. Exit
&amp;gt; 1
Enter amount: 250000
Enter date: 2026/05/23
Choose a category from the following:
給与, 賞与, 副業, その他
Enter category: 給与
Enter description: May salary
Income added.
================================================
Household Budget
================================================
Income:
1: 250000 2026/05/23 給与 May salary
Expenses:
================================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Data is lost when you exit — no persistence. This is purely a learning project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/household_account" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/household_account&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;household_account/
├── src/
│   ├── index.ts             # Entry point / CLI menu
│   ├── account.ts           # Business logic (AccountBook class)
│   ├── types.ts             # Type definitions
│   └── __tests__/
│       └── account.test.ts  # Unit tests (13 cases)
├── jest.config.js
├── tsconfig.json
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three-layer structure as before: &lt;code&gt;types.ts&lt;/code&gt; (types) / &lt;code&gt;account.ts&lt;/code&gt; (logic) / &lt;code&gt;index.ts&lt;/code&gt; (UI).&lt;br&gt;
The key change from the Todo project: &lt;strong&gt;logic moved from functions to a class&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; module)&lt;/li&gt;
&lt;li&gt;Jest + ts-jest (unit testing)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;
&lt;h3&gt;
  
  
  types.ts — Type Definitions
&lt;/h3&gt;

&lt;p&gt;The centerpiece of this project's type design was a &lt;strong&gt;shared interface using generics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My first draft defined &lt;code&gt;Income&lt;/code&gt; and &lt;code&gt;Expense&lt;/code&gt; as completely separate interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// First version — lots of duplication&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Expense&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields — &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; — are identical. AI suggested that generics could consolidate these, and I refactored it myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Refactored with generics&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EntityDetail&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// format: YYYY/MM/DD&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt;  &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;EntityDetail&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Expense&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;EntityDetail&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By passing &lt;code&gt;IncomeCategory&lt;/code&gt; or &lt;code&gt;ExpenseCategory&lt;/code&gt; as the type parameter &lt;code&gt;T&lt;/code&gt;, only the &lt;code&gt;category&lt;/code&gt; field differs between the two. This is the same idea as Java's &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; — a common structure with a pluggable type.&lt;/p&gt;

&lt;p&gt;I also defined the category types and constant lists myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;給与&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;賞与&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;副業&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;その他&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;食費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;交通費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;光熱費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;娯楽&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;その他&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IncomeCategoryList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;給与&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;賞与&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;副業&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;その他&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategoryList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;食費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;交通費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;光熱費&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;娯楽&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;その他&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;Income&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constant lists serve double duty: they're used for both validation and displaying choices to the user.&lt;/p&gt;




&lt;h3&gt;
  
  
  account.ts — The AccountBook Class
&lt;/h3&gt;

&lt;p&gt;My Todo project used &lt;strong&gt;pure functions&lt;/strong&gt; (each returning a new array). This time I switched to a &lt;strong&gt;class-based&lt;/strong&gt; approach, storing state inside the class itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountBook&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;addIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;newIncome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newIncome&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ID generation — learning from last time
&lt;/h4&gt;

&lt;p&gt;In the Todo project, I used &lt;code&gt;todoItems.length + 1&lt;/code&gt; for IDs, which caused duplicates after deletion. This time I introduced &lt;code&gt;nextId&lt;/code&gt; as a dedicated field. It only increments — never resets — so IDs stay unique no matter how many deletions happen.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;delete&lt;/code&gt; — filter to keep everything else
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both income and expenses are checked — the user just provides an ID without specifying which type. The logic is "keep everything that isn't this ID."&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;editIncome&lt;/code&gt; / &lt;code&gt;editExpense&lt;/code&gt; — delete then re-add
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;editIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IncomeCategory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;editedIncome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;editedIncome&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the old record, insert a new one with the same ID. Simple and explicit.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;displayTotal&lt;/code&gt; — &lt;code&gt;reduce&lt;/code&gt; for aggregation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;displayTotal&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalIncome&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalExpense&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalIncome&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;totalExpense&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total income:   &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalIncome&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total expenses: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalExpense&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Balance:        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; — "collapse an array into a single value" — is a natural fit for summing amounts. Same mental model as Java's &lt;code&gt;Stream.reduce()&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;displayIncomeAndExpense&lt;/code&gt; — non-destructive sort with spread
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;displayIncomeAndExpense&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortedIncomes&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortedExpenses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Array.prototype.sort()&lt;/code&gt; mutates the original array. Spreading first (&lt;code&gt;[...array]&lt;/code&gt;) creates a copy, keeping the stored data intact. AI pointed this out — more on that in the "where I got stuck" section.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;isIncomeId&lt;/code&gt; / &lt;code&gt;isExpenseId&lt;/code&gt; — existence check with &lt;code&gt;some&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;isIncomeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;some&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if at least one element matches — Java's &lt;code&gt;Stream.anyMatch()&lt;/code&gt; equivalent. Used to validate that an ID actually exists before editing or deleting.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;getIncome&lt;/code&gt; / &lt;code&gt;getExpense&lt;/code&gt; — added for testing
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added these methods myself to make test assertions easier. &lt;code&gt;find&lt;/code&gt; returns the first matching element, or &lt;code&gt;undefined&lt;/code&gt; if none is found.&lt;/p&gt;




&lt;h3&gt;
  
  
  index.ts — CLI Menu
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Date validation — catching impossible dates
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Format check (YYYY/MM/DD)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}\/\d{2}\/\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dateRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Existence check — catch things like "2026/02/30"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dateObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That date does not exist.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regex can verify the format, but it can't tell you whether February 30th is real. &lt;code&gt;new Date('2026-02-30')&lt;/code&gt; produces an &lt;code&gt;Invalid Date&lt;/code&gt;, and &lt;code&gt;isNaN(dateObj.getTime())&lt;/code&gt; detects that. I added this check myself after noticing the gap.&lt;/p&gt;

&lt;h4&gt;
  
  
  Displaying category options before input
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Choose a category from the following:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IncomeCategoryList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first version just asked for a category without showing options. After running it myself and thinking "wait, what are the valid values?", I added the display. Small UX improvement, self-initiated.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bug fix — &lt;code&gt;||&lt;/code&gt; should be &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; in the delete validation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Passes if the ID exists in either list (always true)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isIncomeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isExpenseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Only errors when the ID exists in neither list&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isIncomeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isExpenseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI caught this one. "Not in income OR not in expense" is always true for any valid ID — the correct logic is AND.&lt;/p&gt;




&lt;h3&gt;
  
  
  account.test.ts — Unit Tests with Jest
&lt;/h3&gt;

&lt;p&gt;Class-based testing requires a fresh instance before each test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AccountBook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addIncome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AccountBook&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;accountBook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountBook&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;can add income&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026/05/23&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;給与&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ID increments after adding income&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026/05/23&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;給与&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026/05/24&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;賞与&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountBook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getIncome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;code&gt;describe.skip&lt;/code&gt; to exclude display methods (&lt;code&gt;console.log&lt;/code&gt;-only) from the test suite.&lt;/p&gt;

&lt;p&gt;Test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; PASS  src/__tests__/account.test.ts
  addIncome
    ✓ can add income
    ✓ ID increments after adding income
  addExpense
    ✓ can add expense
    ✓ ID increments after adding expense
  delete
    ✓ can delete a record
  editIncome
    ✓ can edit income
  editExpense
    ✓ can edit expense
  isIncomeId
    ✓ returns true for a matching income ID
    ✓ returns false for a non-matching income ID
  isExpenseId
    ✓ returns true for a matching expense ID
    ✓ returns false for a non-matching expense ID
  getIncome
    ✓ can retrieve income by ID
  getExpense
    ✓ can retrieve expense by ID

Tests: 13 passed, 13 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spec decisions&lt;/td&gt;
&lt;td&gt;Date format, ID strategy, display format, validation approach — confirmed one by one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design suggestion&lt;/td&gt;
&lt;td&gt;Proposed &lt;code&gt;EntityDetail&lt;/code&gt; generics (&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;) as a way to consolidate the two interfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug catch&lt;/td&gt;
&lt;td&gt;Variable shadowing in &lt;code&gt;editIncome&lt;/code&gt; — callback arg &lt;code&gt;income&lt;/code&gt; was hiding the outer variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug catch&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;main()&lt;/code&gt; calls in certain &lt;code&gt;case&lt;/code&gt; branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug catch&lt;/td&gt;
&lt;td&gt;Logical operator fix in delete validation — changed OR to AND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clarification&lt;/td&gt;
&lt;td&gt;Explained that &lt;code&gt;Array.sort()&lt;/code&gt; mutates the original — suggested the spread-copy pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;sort()&lt;/code&gt; without spread mutated the stored array
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; The display order of records kept changing unexpectedly between views.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;Array.prototype.sort()&lt;/code&gt; is destructive — it modifies the array in place. I was sorting &lt;code&gt;this.account.incomes&lt;/code&gt; directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Mutates the stored array&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Sort a copy, leave the original alone&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortedIncomes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had applied the spread pattern in the Todo project for &lt;code&gt;push&lt;/code&gt;, but hadn't thought to apply it to &lt;code&gt;sort&lt;/code&gt;. AI's reminder made it click for me.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Variable shadowing caused &lt;code&gt;undefined&lt;/code&gt; IDs in &lt;code&gt;editIncome&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; After editing an income record, its ID became &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The callback argument and an outer variable shared the same name — the callback's &lt;code&gt;income&lt;/code&gt; parameter was shadowing the outer scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The filter callback's `income` hides the outer `income` variable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editedIncome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// which `income`?&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Rename the callback argument to avoid the collision&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editedIncome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Income&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can happen in Java too, but it's especially easy to hit in JavaScript where callbacks are everywhere. Good one to internalize.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Regex alone can't catch impossible dates
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; &lt;code&gt;"2026/02/30"&lt;/code&gt; passed format validation and entered the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; A regex can verify &lt;em&gt;structure&lt;/em&gt;, not &lt;em&gt;validity&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Catches wrong format — but not wrong dates&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}\/\d{2}\/\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Catches impossible dates too&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dateObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That date does not exist.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;new Date('2026-02-30')&lt;/code&gt; becomes &lt;code&gt;Invalid Date&lt;/code&gt;, and &lt;code&gt;isNaN(getTime())&lt;/code&gt; catches it. I noticed this gap on my own and added the check.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Forgetting &lt;code&gt;rl.close()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Selecting "Exit" printed the message but the process didn't end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;rl.close()&lt;/code&gt; was missing. Without it, the &lt;code&gt;readline&lt;/code&gt; interface stays open and the process hangs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// required — otherwise the process never exits&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit this same bug in the Todo project. Writing it down again so it sticks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functions vs. Classes
&lt;/h3&gt;

&lt;p&gt;My Todo project used pure functions (return new arrays, no side effects). This project switched to a class.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Function-based&lt;/th&gt;
&lt;th&gt;Class-based&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;Caller holds the variable&lt;/td&gt;
&lt;td&gt;Class holds it internally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;Clear input → output, easy to assert&lt;/td&gt;
&lt;td&gt;Need a fresh instance per test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Familiarity from Java&lt;/td&gt;
&lt;td&gt;More functional-style&lt;/td&gt;
&lt;td&gt;Closer to Java OOP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither is strictly better — it depends on the design goals.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generics &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reuse structure with a pluggable type. &lt;code&gt;EntityDetail&amp;lt;IncomeCategory&amp;gt;&lt;/code&gt; — same idea as &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; in Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interface extends&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inherit shared fields from a base interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional chaining &lt;code&gt;?.&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getIncome(1)?.amount&lt;/code&gt; — safe access when the value might be &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;private&lt;/code&gt; / &lt;code&gt;public&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same as Java — controls access from outside the class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  JavaScript / Node.js
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sort()&lt;/code&gt; is destructive&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Array.sort()&lt;/code&gt; modifies in place — spread first to stay safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the first matching element, or &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable shadowing&lt;/td&gt;
&lt;td&gt;Callback args with the same name as outer vars hide the outer scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date validation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;new Date()&lt;/code&gt; + &lt;code&gt;isNaN(getTime())&lt;/code&gt; catches dates that are formatted correctly but don't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Array Methods vs Java Streams
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().filter()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep elements matching a condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().reduce()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Collapse an array into a single value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;some&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().anyMatch()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return true if any element matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().findFirst()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the first matching element&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Jest Testing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;beforeEach&lt;/code&gt; with class&lt;/td&gt;
&lt;td&gt;Create &lt;code&gt;new AccountBook()&lt;/code&gt; before each test to keep tests independent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expect().toBe()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strict equality check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expect().toBeUndefined()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assert that a value is &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe.skip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Explicitly skip test groups that aren't meaningful to automate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Generics made sense once I had a reason to use them:&lt;/strong&gt; I didn't design with &lt;code&gt;EntityDetail&amp;lt;T&amp;gt;&lt;/code&gt; from the start. I wrote out both interfaces in full, felt the redundancy, and then asked AI if there was a better way. Refactoring &lt;em&gt;after&lt;/em&gt; experiencing the pain made the concept stick much better than if I'd been told about it upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation comes from running the code:&lt;/strong&gt; Both the impossible-date gap and the missing category hints came from actually using the CLI and thinking "that's wrong." The feedback loop of building → using → improving is what I'm finding most valuable in these projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting with types pays off:&lt;/strong&gt; Locking down &lt;code&gt;types.ts&lt;/code&gt; before touching &lt;code&gt;account.ts&lt;/code&gt; meant TypeScript caught mismatches early. This mirrors the interface-first approach I'd use in Java, and it works just as well here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;readline&lt;/code&gt; + &lt;code&gt;async/await&lt;/code&gt; is next:&lt;/strong&gt; The callback nesting in &lt;code&gt;index.ts&lt;/code&gt; was readable this time, but I can see how it'll get messy as complexity grows. Rewriting with &lt;code&gt;async/await&lt;/code&gt; is on my list for the next project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my fourth TypeScript project — a Household Budget CLI.&lt;/p&gt;

&lt;p&gt;Progress since the Todo List:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Designed a shared generic interface (&lt;code&gt;EntityDetail&amp;lt;T&amp;gt;&lt;/code&gt;) from scratch&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moved from function-based to class-based design and understood the trade-offs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used &lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, and &lt;code&gt;find&lt;/code&gt; in real logic — not just exercises&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrote date validation that catches impossible dates, not just malformed ones&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hit and diagnosed variable shadowing — a subtle bug category worth knowing&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next up: an external API tool (weather API) to learn &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, and response type definitions.&lt;/p&gt;

&lt;p&gt;Full learning log: &lt;a href="https://github.com/uya0526-design/household_account/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>jest</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Todo List CLI with TypeScript — interface, Array Methods, and Jest</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Sat, 30 May 2026 05:44:45 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-todo-list-cli-with-typescript-interface-array-methods-and-jest-1k14</link>
      <guid>https://dev.to/uya0526design/building-a-todo-list-cli-with-typescript-interface-array-methods-and-jest-1k14</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my third article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my previous article, I built a Rock-Paper-Scissors CLI and learned about union types, conditional logic, and Jest. This time, I built a &lt;strong&gt;Todo List CLI&lt;/strong&gt; and focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defining data structures with &lt;strong&gt;interface&lt;/strong&gt; (the TypeScript equivalent of a Java POJO class)&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;array methods&lt;/strong&gt; (&lt;code&gt;filter&lt;/code&gt; / &lt;code&gt;map&lt;/code&gt; / &lt;code&gt;reduce&lt;/code&gt; / &lt;code&gt;some&lt;/code&gt;) and comparing them with Java Streams&lt;/li&gt;
&lt;li&gt;Writing &lt;strong&gt;immutable operations&lt;/strong&gt; with the spread operator instead of &lt;code&gt;push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Understanding &lt;strong&gt;async flow in readline&lt;/strong&gt; — callbacks and recursive calls&lt;/li&gt;
&lt;li&gt;Managing test setup with &lt;strong&gt;&lt;code&gt;beforeEach&lt;/code&gt;&lt;/strong&gt; in Jest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as before — I write honestly about where I got stuck, what I figured out, and what I asked AI to help with.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (for design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (for coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;However, I follow these rules for myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI Todo list that manages title, priority, and completion status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm start
&lt;span class="go"&gt;
Select a menu option:
1. Add Todo
2. Delete Todo
3. Toggle completion
4. Show list
0. Exit
1
Enter a title:
Learn TypeScript
Select priority:
1. High
2. Medium
3. Low
1
Todo added!
[ { id: 1, title: 'Learn TypeScript', priority: 'high', completed: false } ]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ The list resets when you exit — no persistence. This is purely a learning project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/todo_list" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/todo_list&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;todo_list/
├── src/
│   ├── index.ts          # Entry point / CLI menu
│   ├── todo.ts           # Todo operation logic
│   ├── types.ts          # Type definitions
│   └── __tests__/
│       └── todo.test.ts  # Unit tests (12 cases)
├── jest.config.js
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same as my previous projects — responsibilities separated by file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; module)&lt;/li&gt;
&lt;li&gt;Jest + ts-jest (unit testing)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  types.ts — Type Definitions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How I used &lt;code&gt;interface&lt;/code&gt; vs &lt;code&gt;type&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially, I embedded the priority options as an inline union type inside the interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// First version (inline)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// embedded directly&lt;/span&gt;
  &lt;span class="nl"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I built out &lt;code&gt;index.ts&lt;/code&gt;, I realized I needed to reference the priority values there too. So I refactored &lt;code&gt;Priority&lt;/code&gt; into its own named &lt;code&gt;type&lt;/code&gt; alias — a decision I made myself, without being prompted.&lt;/p&gt;

&lt;p&gt;Naming the type makes the intent clearer: &lt;em&gt;this value represents a priority level&lt;/em&gt;, not just a random string.&lt;/p&gt;




&lt;h3&gt;
  
  
  todo.ts — Todo Operation Logic
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;addTodoItem&lt;/code&gt; function
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Priority&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Priority&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newItem&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spread instead of &lt;code&gt;push&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;push&lt;/code&gt; mutates the original array. Returning a new array with &lt;code&gt;[...todoItems, newItem]&lt;/code&gt; keeps the function pure and the data immutable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reduce&lt;/code&gt; for ID generation&lt;/strong&gt;: Using &lt;code&gt;todoItems.length + 1&lt;/code&gt; caused duplicate IDs after deletion. Using &lt;code&gt;reduce&lt;/code&gt; to find the current max ID solves that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was the first time I used &lt;code&gt;reduce&lt;/code&gt; in real logic. The mental model — "collapse an array into a single value" — maps directly to Java's &lt;code&gt;Stream.reduce()&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;deleteTodoItem&lt;/code&gt; function
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Keep everything except the one to remove" — clean and immutable. The original array is never touched.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;toggleTodoItem&lt;/code&gt; function
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What &lt;code&gt;{ ...item, completed: !item.completed }&lt;/code&gt; does:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of doing &lt;code&gt;item.completed = !item.completed&lt;/code&gt; (which mutates), this creates a new object: spread all properties from &lt;code&gt;item&lt;/code&gt;, then override only &lt;code&gt;completed&lt;/code&gt;. The original object is untouched.&lt;/p&gt;

&lt;p&gt;This style felt unfamiliar coming from Java, but once I saw it as "copy and override," it clicked.&lt;/p&gt;




&lt;h3&gt;
  
  
  index.ts — CLI Menu
&lt;/h3&gt;

&lt;p&gt;The main loop uses &lt;code&gt;readline&lt;/code&gt; + &lt;code&gt;switch&lt;/code&gt;, with recursion to keep the menu running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggleTodoItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./todo.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Priority&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInterface&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Select a menu option:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1. Add Todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2. Delete Todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3. Toggle completion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4. Show list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0. Exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please enter a number.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;deleteTodo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;toggleTodo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;let&lt;/code&gt; instead of &lt;code&gt;const&lt;/code&gt; for &lt;code&gt;todoItems&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since my Todo functions return new arrays (immutable pattern), I need to reassign the variable with the returned value. &lt;code&gt;const&lt;/code&gt; doesn't allow reassignment, so &lt;code&gt;let&lt;/code&gt; is needed here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ const doesn't allow reassignment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Error&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ let allows reassignment&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  todo.test.ts — Unit Tests with Jest
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;beforeEach&lt;/code&gt; to reset state before each test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggleTodoItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addTodoItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adding a Todo increases the count by 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;added Todo has id 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;added Todo has the correct title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;added Todo has the correct priority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;added Todo has completed = false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adding after delete does not produce a duplicate ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;addTodoItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleting a Todo decreases the count by 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleted Todo no longer exists in the list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeUndefined&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;original array is unchanged after delete (immutability)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;deleteTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;addTodoItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggling changes completed from false to true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggling twice returns completed to false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toggled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggling one item does not affect others&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toggleTodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; PASS  src/__tests__/todo.test.ts
  addTodoItem
    ✓ adding a Todo increases the count by 1
    ✓ added Todo has id 1
    ✓ added Todo has the correct title
    ✓ added Todo has the correct priority
    ✓ added Todo has completed = false
    ✓ adding after delete does not produce a duplicate ID
    ✓ deleting a Todo decreases the count by 1
    ✓ deleted Todo no longer exists in the list
    ✓ original array is unchanged after delete (immutability)
    ✓ toggling changes completed from false to true
    ✓ toggling twice returns completed to false
    ✓ toggling one item does not affect others

Tests: 12 passed, 12 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;interface syntax&lt;/td&gt;
&lt;td&gt;The basic &lt;code&gt;export interface Name { ... }&lt;/code&gt; form and PascalCase convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;push → spread&lt;/td&gt;
&lt;td&gt;The concept of immutability and the &lt;code&gt;[...arr, newItem]&lt;/code&gt; approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive &lt;code&gt;main()&lt;/code&gt; issue&lt;/td&gt;
&lt;td&gt;Explained that &lt;code&gt;rl.question&lt;/code&gt; is async — calling &lt;code&gt;main()&lt;/code&gt; outside the callback runs immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;switch&lt;/code&gt; fallthrough&lt;/td&gt;
&lt;td&gt;Pointed out that missing &lt;code&gt;break&lt;/code&gt; / &lt;code&gt;return&lt;/code&gt; causes the next &lt;code&gt;case&lt;/code&gt; to run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing after &lt;code&gt;isNaN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Noted that without &lt;code&gt;return&lt;/code&gt;, code after the check still executes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest setup&lt;/td&gt;
&lt;td&gt;Explained &lt;code&gt;npm install --save-dev jest ts-jest @types/jest&lt;/code&gt; and what each package does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;beforeEach&lt;/code&gt; usage&lt;/td&gt;
&lt;td&gt;Explained the syntax, compared to JUnit's &lt;code&gt;@BeforeEach&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test coverage ideas&lt;/td&gt;
&lt;td&gt;Suggested what edge cases to verify for &lt;code&gt;deleteTodoItem&lt;/code&gt; and &lt;code&gt;toggleTodoItem&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Adding after deletion caused duplicate IDs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; After deleting the item with &lt;code&gt;id: 1&lt;/code&gt;, adding a new item also got &lt;code&gt;id: 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; My original ID logic was &lt;code&gt;todoItems.length + 1&lt;/code&gt;. After a deletion, the array length decreases — so the new ID could collide with one that already exists elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Length-based (breaks after deletion)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Max ID-based (always safe)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Insight:&lt;/strong&gt; I only caught this because I wrote a test for it. Without that test case, I might not have noticed until it caused a real bug.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Calling &lt;code&gt;main()&lt;/code&gt; outside the &lt;code&gt;rl.question&lt;/code&gt; callback
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I wanted to show the menu again after each selection, so I initially wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Runs immediately, doesn't wait for input&lt;/span&gt;
&lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... handle input&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// called outside the callback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;rl.question()&lt;/code&gt; is asynchronous — it registers a callback and moves on immediately. Calling &lt;code&gt;main()&lt;/code&gt; outside the callback means it executes right away, before the user even answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Move every &lt;code&gt;main()&lt;/code&gt; call &lt;em&gt;inside&lt;/em&gt; the callback, at the end of each &lt;code&gt;case&lt;/code&gt; branch.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Missing &lt;code&gt;break&lt;/code&gt; in the &lt;code&gt;default&lt;/code&gt; case
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; When a user entered an invalid option, &lt;code&gt;main()&lt;/code&gt; was called (to show the menu again), but then execution fell through into &lt;code&gt;case 0&lt;/code&gt; and closed the program.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; No &lt;code&gt;break&lt;/code&gt; after the &lt;code&gt;default&lt;/code&gt; block. JavaScript's &lt;code&gt;switch&lt;/code&gt; falls through to the next &lt;code&gt;case&lt;/code&gt; just like Java — you need &lt;code&gt;break&lt;/code&gt; or &lt;code&gt;return&lt;/code&gt; to stop it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Falls through to case 0&lt;/span&gt;
&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// execution continues into case 0...&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Stops here&lt;/span&gt;
&lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know this rule from Java, but it's easy to forget in the heat of writing logic.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. No error when specifying a non-existent ID
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Trying to delete or toggle an ID that didn't exist produced no error and no feedback — the operation just silently did nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Added an existence check using &lt;code&gt;some()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No Todo found with that ID.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;some()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if at least one element matches the condition — equivalent to Java's &lt;code&gt;Stream.anyMatch()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TypeScript Type System
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interface&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the &lt;em&gt;shape&lt;/em&gt; of data — similar to a Java POJO class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;type&lt;/code&gt; alias&lt;/td&gt;
&lt;td&gt;Gives a name to a union type for reuse across files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shorthand properties&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ title: title }&lt;/code&gt; can be written as &lt;code&gt;{ title }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Array Methods vs Java Streams
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().filter()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep elements that match a condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().map()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transform every element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().reduce()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Collapse an array into a single value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;some&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream().anyMatch()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return true if any element matches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Immutable Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;push&lt;/code&gt; mutates the original array → use &lt;code&gt;[...arr, newItem]&lt;/code&gt; to return a new one&lt;/li&gt;
&lt;li&gt;Partial object updates → use &lt;code&gt;{ ...obj, field: newValue }&lt;/code&gt; to return a new object&lt;/li&gt;
&lt;li&gt;Functions that return new values instead of mutating are easier to test&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Async Flow with readline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rl.question()&lt;/code&gt; registers a callback and returns immediately&lt;/li&gt;
&lt;li&gt;To loop the menu, call &lt;code&gt;main()&lt;/code&gt; recursively &lt;em&gt;inside&lt;/em&gt; the callback — not outside&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Jest Testing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeEach&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs before each test — equivalent to JUnit's &lt;code&gt;@BeforeEach&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toHaveLength(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asserts array length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toBeUndefined()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asserts a value is &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutability testing&lt;/td&gt;
&lt;td&gt;After an operation, verify the original array hasn't changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where my Java background helped:&lt;/strong&gt; The Java Stream API background made &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, and &lt;code&gt;some&lt;/code&gt; immediately intuitive — I could connect each one to its Java counterpart. My quality-first instincts also kicked in: I wrote tests for "does deletion preserve IDs?" and "does the operation stay immutable?" without being asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Biggest new concept:&lt;/strong&gt; Async readline. Coming from Java's synchronous execution model, the idea that code after &lt;code&gt;rl.question()&lt;/code&gt; runs &lt;em&gt;before&lt;/em&gt; the callback was genuinely surprising. Hitting the bug and debugging it was the best way to understand it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-initiated refactor:&lt;/strong&gt; Extracting &lt;code&gt;Priority&lt;/code&gt; into its own &lt;code&gt;type&lt;/code&gt; alias was my own call. I noticed the need when building &lt;code&gt;index.ts&lt;/code&gt;, and made the change without prompting — that kind of design awareness feels like real progress.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my third TypeScript project — a Todo List CLI.&lt;/p&gt;

&lt;p&gt;Progress since the Rock-Paper-Scissors game:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Designed a multi-field data structure with &lt;code&gt;interface&lt;/code&gt; on my own&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, and &lt;code&gt;some&lt;/code&gt; in real logic&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applied immutable patterns consistently throughout&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understood async callback flow by hitting (and fixing) a real bug&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Managed test state properly with &lt;code&gt;beforeEach&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next up: persistent storage (writing to a file) and deeper async patterns.&lt;/p&gt;

&lt;p&gt;The full learning log is in &lt;a href="https://github.com/uya0526-design/todo_list/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>jest</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Rock-Paper-Scissors CLI with TypeScript — Union Types, Conditionals, and Jest</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Mon, 25 May 2026 13:46:33 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-rock-paper-scissors-cli-with-typescript-union-types-conditionals-and-jest-2lc5</link>
      <guid>https://dev.to/uya0526design/building-a-rock-paper-scissors-cli-with-typescript-union-types-conditionals-and-jest-2lc5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my second article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;In my first article, I built a BMI Calculator CLI and learned about union types, function separation, and Vitest. This time, I built a &lt;strong&gt;Rock-Paper-Scissors game&lt;/strong&gt; (CLI) and focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expressing game "hands" and "results" with &lt;strong&gt;union types&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Understanding when to use &lt;code&gt;if&lt;/code&gt; vs &lt;code&gt;else if&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Writing unit tests with &lt;strong&gt;Jest + ts-jest&lt;/strong&gt; (I used Vitest last time — this time I wrestled with Jest)&lt;/li&gt;
&lt;li&gt;Understanding the difference between the &lt;strong&gt;nullish coalescing operator &lt;code&gt;??&lt;/code&gt;&lt;/strong&gt; and type assertion &lt;code&gt;as&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as before — I write honestly about what I struggled with, what I figured out, and what I asked AI to help with.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (for design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (for coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;However, I follow these rules for myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI Rock-Paper-Scissors game where you play against the computer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm start
&lt;span class="go"&gt;
Enter your hand (rock, paper, scissors): rock
Player: rock
Computer: scissors
Result: win
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid input (anything other than &lt;code&gt;rock&lt;/code&gt;, &lt;code&gt;paper&lt;/code&gt;, or &lt;code&gt;scissors&lt;/code&gt;) is rejected with a validation message.&lt;/p&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/janken-game" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/janken-game&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;janken-game/
├── src/
│   ├── index.ts           # Entry point / CLI I/O
│   ├── game.ts            # Game logic (result determination, CPU hand generation)
│   ├── types.ts           # Type definitions
│   └── __tests__/
│       └── game.test.ts   # Unit tests (all 9 combinations)
├── jest.config.js
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same as my BMI project — responsibilities are separated by file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; module)&lt;/li&gt;
&lt;li&gt;Jest + ts-jest (unit testing)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  types.ts — Type Definitions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose this design myself, based on experience from the previous project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why union types instead of enum?&lt;/strong&gt;&lt;br&gt;
With &lt;code&gt;enum&lt;/code&gt;, the underlying values are numbers (&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;), which makes logs harder to read. With union types, the string values appear as-is — cleaner for debugging and more idiomatic TypeScript.&lt;/p&gt;

&lt;p&gt;I also added &lt;code&gt;export&lt;/code&gt; myself, knowing these types would be needed in other files.&lt;/p&gt;


&lt;h3&gt;
  
  
  game.ts — &lt;code&gt;determineResult&lt;/code&gt; function
&lt;/h3&gt;

&lt;p&gt;All 9 combinations (3×3) of win/lose/draw logic live here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;     &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;if&lt;/code&gt; vs &lt;code&gt;else if&lt;/code&gt; — where I got it wrong first:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My initial version looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// First attempt (not ideal)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ... all 9 combinations as separate if statements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because of the &lt;code&gt;return&lt;/code&gt; statements, but the intent is unclear — after matching &lt;code&gt;'draw'&lt;/code&gt;, the other &lt;code&gt;if&lt;/code&gt; blocks still get evaluated. AI pointed out that &lt;strong&gt;mutually exclusive conditions should use &lt;code&gt;else if&lt;/code&gt;&lt;/strong&gt; to signal that only one branch can be true. That feedback led me to the cleaner version above.&lt;/p&gt;




&lt;h3&gt;
  
  
  game.ts — &lt;code&gt;getComputerHand&lt;/code&gt; function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getComputerHand&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;code&gt;hands.length&lt;/code&gt; instead of hardcoding &lt;code&gt;* 3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Chose &lt;code&gt;as Hand&lt;/code&gt; over &lt;code&gt;?? 'rock'&lt;/code&gt; after understanding the difference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nullish coalescing operator &lt;code&gt;??&lt;/code&gt; returns the right-hand side only when the left is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;. Since &lt;code&gt;hands[index]&lt;/code&gt; is always a valid &lt;code&gt;Hand&lt;/code&gt; value within bounds, a fallback isn't needed. I used &lt;code&gt;as Hand&lt;/code&gt; instead to explicitly tell the compiler "this is safe, I've validated it."&lt;/p&gt;




&lt;h3&gt;
  
  
  index.ts — CLI Input/Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getComputerHand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./game.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInterface&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validHands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter your hand (rock, paper, scissors): &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validHands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input. Please enter rock, paper, or scissors.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playerHand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;computerHand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputerHand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerHand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computerHand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;playerHand&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Computer: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;computerHand&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lesson from the previous project: always call &lt;code&gt;rl.close()&lt;/code&gt; in every exit path. This time I made sure both the validation failure path and the happy path close properly.&lt;/p&gt;




&lt;h3&gt;
  
  
  game.test.ts — Unit Tests with Jest
&lt;/h3&gt;

&lt;p&gt;The goal: cover all 9 combinations (3 wins + 3 losses + 3 draws).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;determineResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../game&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;determineResult&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Win (3 patterns)&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock beats scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors beats paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper beats rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Lose (3 patterns)&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock loses to paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors loses to rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper loses to scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw (3 patterns)&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock vs rock is a draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors vs scissors is a draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scissors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper vs paper is a draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;determineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; PASS  src/__tests__/game.test.ts
  determineResult
    ✓ rock beats scissors
    ✓ scissors beats paper
    ✓ paper beats rock
    ✓ rock loses to paper
    ✓ scissors loses to rock
    ✓ paper loses to scissors
    ✓ rock vs rock is a draw
    ✓ scissors vs scissors is a draw
    ✓ paper vs paper is a draw

Tests: 9 passed, 9 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundary value mindset I developed in the BMI project carried over here — for Rock-Paper-Scissors, full coverage means all 9 combinations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What AI helped with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hand type design&lt;/td&gt;
&lt;td&gt;Introduced me to union types and string literal types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;if&lt;/code&gt; vs &lt;code&gt;else if&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pointed out that mutually exclusive conditions should use &lt;code&gt;else if&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU random hand&lt;/td&gt;
&lt;td&gt;Suggested the &lt;code&gt;Math.random()&lt;/code&gt; + array approach and flagged the need for type assertion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;rl.close()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Spotted that the happy path was missing &lt;code&gt;rl.close()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest basics&lt;/td&gt;
&lt;td&gt;Showed me the &lt;code&gt;describe / test / expect&lt;/code&gt; structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Jest TypeScript error: &lt;code&gt;TS5107: moduleResolution=node10 deprecated&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Running &lt;code&gt;npm test&lt;/code&gt; gave this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error TS5107: Option 'moduleResolution' value 'node10' is deprecated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Investigation:&lt;/strong&gt; I read the error message and figured out that &lt;code&gt;tsconfig.json&lt;/code&gt; was using an outdated setting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Updated &lt;code&gt;module&lt;/code&gt; and &lt;code&gt;moduleResolution&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; to &lt;code&gt;nodenext&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important caveat:&lt;/strong&gt; I kept &lt;code&gt;"type": "commonjs"&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;. Changing it to &lt;code&gt;"module"&lt;/code&gt; caused Jest compatibility errors. These two settings are independent — &lt;code&gt;tsconfig.json&lt;/code&gt; controls the TypeScript compiler, while &lt;code&gt;package.json&lt;/code&gt; controls how Node.js handles &lt;code&gt;.js&lt;/code&gt; files at runtime. The right combination depends on the tooling version.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Three losses in a row — is &lt;code&gt;Math.random&lt;/code&gt; broken?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; After losing 3 times in a row out of 5 games, I started to wonder if the CPU hand wasn't actually random.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investigation:&lt;/strong&gt; I re-read the code — the implementation was correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insight:&lt;/strong&gt; With only 5 samples, variance is completely expected (law of large numbers). Over 100 games the distribution would even out. This wasn't a bug — it was a statistics lesson.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. &lt;code&gt;.js&lt;/code&gt; extension required with &lt;code&gt;moduleResolution: nodenext&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; Importing from &lt;code&gt;types.ts&lt;/code&gt; threw this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot find module './types'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; With &lt;code&gt;moduleResolution: nodenext&lt;/code&gt;, TypeScript requires &lt;code&gt;.js&lt;/code&gt; extensions even in &lt;code&gt;.ts&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Does not work&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Works&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason: TypeScript assumes you're referencing the compiled output (&lt;code&gt;.js&lt;/code&gt;), not the source file (&lt;code&gt;.ts&lt;/code&gt;). It felt strange at first, but once I understood the reasoning it made sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TypeScript Type System
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Union types&lt;/td&gt;
&lt;td&gt;Define a fixed set of allowed values as a type (e.g. Hand limited to rock, paper, or scissors)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String literal types&lt;/td&gt;
&lt;td&gt;The string itself becomes the type. Lighter than enum and more idiomatic TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type assertion &lt;code&gt;as&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tells the compiler "trust me, I've validated this" — use only after validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Imports type information only — not included in the runtime bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Conditional Logic
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;if&lt;/code&gt; vs &lt;code&gt;else if&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mutually exclusive conditions should use &lt;code&gt;else if&lt;/code&gt; to make intent clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early return&lt;/td&gt;
&lt;td&gt;Returning early on invalid input keeps nesting shallow and logic readable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Nullish Coalescing &lt;code&gt;??&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Returns the right-hand side only when the left is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;. Unlike &lt;code&gt;||&lt;/code&gt;, it does &lt;em&gt;not&lt;/em&gt; trigger on &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;""&lt;/code&gt;. Useful for fallback values when undefined is the only concern — but not needed when array bounds are guaranteed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jest Unit Testing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groups related tests (similar to &lt;code&gt;@Nested&lt;/code&gt; in JUnit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines an individual test case (similar to &lt;code&gt;@Test&lt;/code&gt; in JUnit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expect(...).toBe(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strict equality check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full coverage mindset&lt;/td&gt;
&lt;td&gt;Rock-Paper-Scissors has 3×3 = 9 combinations — covering all of them gives confidence in the logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Module System Configuration
&lt;/h3&gt;

&lt;p&gt;A deeper understanding than the previous project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tsconfig.json&lt;/code&gt; (&lt;code&gt;module&lt;/code&gt; / &lt;code&gt;moduleResolution&lt;/code&gt;) — controls the TypeScript compiler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; (&lt;code&gt;"type"&lt;/code&gt;) — controls how Node.js interprets &lt;code&gt;.js&lt;/code&gt; files at runtime&lt;/li&gt;
&lt;li&gt;These are independent settings; the right combination depends on your toolchain&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where my Java background helped:&lt;/strong&gt; I naturally structured the logic before writing code (design-first thinking). Writing all 9 test cases proactively also reflects a quality-first mindset from Java development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I got tripped up:&lt;/strong&gt; The module resolution configuration (&lt;code&gt;tsconfig.json&lt;/code&gt; vs &lt;code&gt;package.json&lt;/code&gt;) has no real equivalent in Java. I was confused at first, but reading the error messages carefully led me to the fix — which felt like a real debugging win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "is this a bug?" moment:&lt;/strong&gt; Suspecting &lt;code&gt;Math.random&lt;/code&gt; after 3 losses taught me not to draw conclusions from small samples. That's a useful development instinct to build early.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my second TypeScript project — a Rock-Paper-Scissors CLI.&lt;/p&gt;

&lt;p&gt;Progress since the BMI calculator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Chose union types on my own, without being prompted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understood &lt;code&gt;else if&lt;/code&gt; and used it to express intent clearly&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Designed and implemented all 9 test combinations myself&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read an error message, investigated, and fixed it independently&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full learning log is in &lt;a href="https://github.com/uya0526-design/janken-game/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>jest</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a BMI Calculator CLI with TypeScript — Types, Functions, and Vitest</title>
      <dc:creator>Uya</dc:creator>
      <pubDate>Sat, 23 May 2026 16:05:50 +0000</pubDate>
      <link>https://dev.to/uya0526design/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest-2dbb</link>
      <guid>https://dev.to/uya0526design/building-a-bmi-calculator-cli-with-typescript-types-functions-and-vitest-2dbb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my first article as a Java engineer learning TypeScript from scratch.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Learning Style (AI Transparency)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I use &lt;strong&gt;Claude Pro&lt;/strong&gt; (for design discussions and Q&amp;amp;A) and &lt;strong&gt;Cursor Pro&lt;/strong&gt; (for coding support) as learning companions.&lt;/p&gt;

&lt;p&gt;However, I follow these rules for myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I write all the code myself&lt;/strong&gt; — I never ask AI to write code for me&lt;/li&gt;
&lt;li&gt;AI helps with hints, spec clarification, and bug spotting&lt;/li&gt;
&lt;li&gt;I make sure I understand &lt;em&gt;why&lt;/em&gt; something works before moving on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I clearly separate "what I implemented myself" from "what I asked AI for."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A CLI tool that calculates BMI from height and weight input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm start

Enter your height &lt;span class="o"&gt;(&lt;/span&gt;cm&lt;span class="o"&gt;)&lt;/span&gt;: 170
Enter your weight &lt;span class="o"&gt;(&lt;/span&gt;kg&lt;span class="o"&gt;)&lt;/span&gt;: 70
BMI result: 24.22 &lt;span class="o"&gt;(&lt;/span&gt;Normal&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BMI Classification&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BMI&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 18.5&lt;/td&gt;
&lt;td&gt;Underweight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18.5 – 24.9&lt;/td&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥ 25.0&lt;/td&gt;
&lt;td&gt;Obese&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;📦 Repository: &lt;a href="https://github.com/uya0526-design/bmi-calculator" rel="noopener noreferrer"&gt;https://github.com/uya0526-design/bmi-calculator&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separating responsibilities by file made it much clearer where everything lived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js (&lt;code&gt;readline&lt;/code&gt; module)&lt;/li&gt;
&lt;li&gt;Vitest (unit testing)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Implemented Myself
&lt;/h2&gt;

&lt;h3&gt;
  
  
  types.ts — Type Definitions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Type aliases for height, weight, and BMI value&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BmiValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Union type for classification labels&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BmiLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Underweight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Obese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Object type to hold the calculation result&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BmiOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BmiValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BmiLabel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key decision here was using a &lt;strong&gt;union type&lt;/strong&gt; for &lt;code&gt;BmiLabel&lt;/code&gt;. Any string outside of &lt;code&gt;"Underweight" | "Normal" | "Obese"&lt;/code&gt; causes a type error at compile time.&lt;/p&gt;

&lt;h3&gt;
  
  
  calculator.ts — Calculation Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BmiOutput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBmiLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;18.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Underweight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Obese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;BmiOutput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heightInM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bmi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;heightInM&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBmiLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  &lt;span class="c1"&gt;// shorthand property notation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I decided not to &lt;code&gt;export&lt;/code&gt; &lt;code&gt;getBmiLabel&lt;/code&gt; since it's only used internally — and I made that call myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  index.ts — CLI Input/Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;calculateBmi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./calculator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInterface&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter your height (cm): &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heightInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heightInput&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a number.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter your weight (kg): &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weightInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weightInput&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a number.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heightInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weightInput&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`BMI result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// ← position matters (see "Where I Got Stuck")&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  calculator.test.ts — Unit Tests with Vitest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;calculateBmi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../calculator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calculateBmi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BMI 18.49 → Underweight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;53.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Underweight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BMI 18.5 → Normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;53.52&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BMI 25 or above → Obese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;72.25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Obese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BMI calculation accuracy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateBmi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bmi&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeCloseTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;24.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately chose boundary values (18.49 / 18.5 / 25) to verify the branching logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Asked AI For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type design thinking&lt;/td&gt;
&lt;td&gt;Asked about the difference between tuple types and object types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tsconfig.json options&lt;/td&gt;
&lt;td&gt;Learned what &lt;code&gt;module&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt;, and &lt;code&gt;strict&lt;/code&gt; each do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vitest setup&lt;/td&gt;
&lt;td&gt;Confirmed the setup steps and &lt;code&gt;package.json&lt;/code&gt; config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test design principles&lt;/td&gt;
&lt;td&gt;Learned the difference between &lt;code&gt;toBe&lt;/code&gt; and &lt;code&gt;toBeOneOf&lt;/code&gt;, and the one-case-per-test rule&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;npm&lt;/code&gt; wouldn't run in PowerShell
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; The default execution policy was &lt;code&gt;Restricted&lt;/code&gt; (no scripts allowed).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RemoteSigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Process&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;-Scope Process&lt;/code&gt; applies the change only to the current session — no permanent system changes.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Mismatch between &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;"type"&lt;/code&gt; and &lt;code&gt;tsconfig.json&lt;/code&gt; &lt;code&gt;"module"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; &lt;code&gt;package.json&lt;/code&gt; had &lt;code&gt;"type": "module"&lt;/code&gt; but &lt;code&gt;tsconfig.json&lt;/code&gt; had &lt;code&gt;"module": "commonjs"&lt;/code&gt; — they were out of sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Changed &lt;code&gt;package.json&lt;/code&gt; to &lt;code&gt;"type": "commonjs"&lt;/code&gt;. These two settings always need to match.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. &lt;code&gt;rl.close()&lt;/code&gt; in the wrong place
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Situation:&lt;/strong&gt; I put &lt;code&gt;rl.close()&lt;/code&gt; at the end of the outer callback, but it was closing readline before the inner &lt;code&gt;rl.question&lt;/code&gt; could finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insight:&lt;/strong&gt; &lt;code&gt;rl.question&lt;/code&gt; is asynchronous. Writing code &lt;em&gt;after&lt;/em&gt; it doesn't mean it runs &lt;em&gt;after&lt;/em&gt; the callback completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Moved &lt;code&gt;rl.close()&lt;/code&gt; inside the inner callback, after all processing is done.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TypeScript
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Union types&lt;/td&gt;
&lt;td&gt;Union types restrict strings to allowed values, catching mistakes at compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limits of type aliases&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Height = number&lt;/code&gt; and &lt;code&gt;Weight = number&lt;/code&gt; are both just &lt;code&gt;number&lt;/code&gt; at runtime — swapping arguments doesn't cause a type error (Branded Types can fix this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Explicitly imports types only — useful with &lt;code&gt;verbatimModuleSyntax&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shorthand properties&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ bmi, label }&lt;/code&gt; works when variable names match property names&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Testing (Vitest)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript types don't exist at runtime&lt;/td&gt;
&lt;td&gt;Testing types with Vitest is unnecessary — the compiler already guarantees them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;toBe&lt;/code&gt; vs &lt;code&gt;toBeOneOf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;toBeOneOf&lt;/code&gt; passes if &lt;em&gt;any&lt;/em&gt; value matches — it can't verify correctness. Use &lt;code&gt;toBe&lt;/code&gt; with a specific expected value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary value testing&lt;/td&gt;
&lt;td&gt;Testing around thresholds (18.5, 25) verifies that branching logic is correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe.skip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keeps the file in place while skipping tests — useful for preserving learning notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was my first TypeScript project — a simple BMI calculator CLI.&lt;/p&gt;

&lt;p&gt;Two things stood out from this experience:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Designing types first made implementation smoother&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async callbacks are easy to misplace — position matters&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next up: a &lt;strong&gt;Rock-Paper-Scissors game&lt;/strong&gt; (union types, conditionals, enums).&lt;/p&gt;

&lt;p&gt;The full learning log is in &lt;a href="https://github.com/uya0526-design/bmi-calculator/blob/main/LEARNING_LOG.md" rel="noopener noreferrer"&gt;LEARNING_LOG.md&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>vitest</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
