When Type Systems Feel Short
Written by
At
noidilin
Fri Mar 13 2026

A date range picker bug looked small at first.
A user selected the same start and end date, something like 02-20 -> 02-20, and expected to see every record from that day. The UI looked correct. The picker returned valid JavaScript Date objects. The frontend sent valid ISO strings. The backend accepted them.
Everything was technically valid.
And still, records from later on 02-20 were missing.
That was the uncomfortable part of the bug for me. My first instinct was to look at the component: maybe the date picker was wrong, maybe the form value was wrong, maybe something was off in the display. But after tracing the value across the system, the picker was not the real problem.
The problem was that a valid Date value crossed a boundary while carrying the wrong meaning.
What I wanted to understand was:
- Why did a same-day filter exclude records from that same day?
- What did the UI value mean compared with the API value?
- Where should timezone and business-day semantics be encoded?
- Why did the type system not protect us from this bug?
What I learned was that frontend bugs are often not caused by invalid values. They are caused by valid values whose meaning changes between layers.
The mental model I want to keep is:
UI state stores intent
transport formats store protocol
boundary mappers preserve meaningIn this case, the missing boundary was between a user-selected calendar date range and API-ready ISO instants.
What the user meant
When the user selected:
02-20 -> 02-20They were not thinking about timestamps.
They meant:
show me records from the full business day of February 20For this admin app, there was also a business rule hiding behind that sentence: calendar dates should be interpreted in Asia/Tokyo.
So the user's intent was more precisely:
from 2026-02-20 00:00:00.000 Asia/Tokyo
to 2026-02-20 23:59:59.999 Asia/TokyoBut that meaning was not represented explicitly in the value we sent to the API.
The frontend had Date objects from the picker and called toISOString() on them. That created valid ISO timestamps, but it did not encode the rule that the end date should mean the end of the Tokyo calendar day.
So some endpoints interpreted the end value as an exact instant near the start of the day. Records later on that date fell outside the filter.
What the API received
The API did not receive “the full Tokyo business day.”
It received exact ISO instants.
That is a different contract.
The UI model was:
selected calendar date rangeThe API contract was:
inclusive ISO timestamp boundariesThose two models are related, but they are not interchangeable.
That was the core mistake. I treated Date -> ISO string as a harmless conversion because both sides were valid types. But the conversion skipped the business rule that gave the date range its meaning.
This was not just a timezone bug. It was a boundary bug.
The fix: make the boundary explicit
The fix was to introduce a small translation layer in src/lib/date-range.ts.
That layer had two jobs:
serializeDateRange()converts picker dates into inclusive Tokyo day boundaries for the APIparseDateRange()converts stored ISO values back into picker-friendlyDateobjects while preserving the Tokyo calendar day
The important part was not the amount of code. The important part was the ownership.
Instead of letting the form or request code casually call toISOString(), the boundary layer became the place where the meaning is made explicit:
picker date range -> Tokyo business-day range -> API ISO timestampsAnd in the other direction:
stored ISO timestamps -> Tokyo calendar days -> picker valuesThat round trip matters. If the UI cannot reconstruct the same user intent after saving, loading, and parsing the data, then the boundary is leaking meaning.
Why the type system did not save us
This bug made the title of the post feel real to me: sometimes the type system feels short.
Not useless. Just short of the actual problem.
A JavaScript Date can represent an instant. It can also be used by UI code to represent a selected calendar day. Those are very different concepts, but the type name is the same.
An ISO string is valid transport data. But it does not say whether it represents:
- an exact event time
- the start of a business day
- the end of a business day
- a UTC boundary
- a Tokyo calendar boundary
The type can say “this is a string” or “this is a Date.” It cannot automatically say “this value still means what the user meant when they clicked February 20.”
That is the gap where the bug lived.
The frontend lesson
The useful shift for me was this:
The UI model is not the API model.
The UI should capture user intent. The API contract should capture what the backend expects. If those two shapes are different, passing values through directly is not simplicity. It is an unowned boundary.
Before this bug, I might have looked at the flow and thought:
- picker returns
Date - request needs ISO string
- call
toISOString() - done
Now I want to ask different questions:
- Is this
Datean instant or a calendar day? - Which timezone defines that calendar day?
- Is the end boundary inclusive?
- Does the backend interpret the value the same way the user does?
- Can the UI round-trip the value and recover the same intent?
That feels like a more mature frontend habit. Not because it adds complexity everywhere, but because it notices where meaning can be lost.
A checklist for next time
When I run into similar boundary bugs, I want to trace the value through the whole system:
- What does the user think they selected or entered?
- What exact value does the UI store?
- What exact value is sent over the network?
- What does the backend think that value means?
- When the data comes back, can the UI reconstruct the same user intent?
If the answer to the last question is no, the round trip is broken.
That usually means the boundary layer is missing, too thin, or mixing transport details with domain meaning.
What I want to carry forward
The concrete bug was a same-day date range filter. The reusable lesson was about meaning at frontend boundaries.
My refined model is:
- establish the canonical business meaning first
- keep UI models focused on user intent
- keep API contracts focused on transport requirements
- add explicit serializer/parser helpers at the boundary
- test the full round trip, not just the individual values
The mistake was not using Date. The mistake was assuming a technically valid conversion also preserved the user's intent.
That is what I want to remember: types are necessary, but they are not the whole design. A value can pass every type check and still mean the wrong thing after it crosses a boundary.
The unresolved question I want to keep exploring is how far to push the type system here. Should concepts like TokyoCalendarDate, InclusiveDateRange, or ApiInstant become explicit types in the codebase, or is a well-owned boundary mapper enough for most frontend work?