When working with forms in TypeScript, it’s easy to assume these two types are almost identical:
busFare?: number
and
busFare: number | undefined
At first glance, they look very similar.
But in real applications, especially when working with forms, APIs, and database updates, the difference matters much more than it seems.
This subtle distinction caused a bug in my update logic where old values remained in the database unexpectedly.
This article explains:
- the difference between
?:and| undefined - why
JSON.stringify()can cause unexpected behavior - why
nullmay be a better choice for persistence - how discriminated unions can model business rules more safely
Table of Contents
- The Business Requirement
?: numbervsnumber | undefined- The Hidden
JSON.stringify()Problem - How Old Data Stayed in the Database
- The Type Problem: Invalid Business States
- A Better Approach: Model Valid States
- Why
nullWorked Better - Final Thoughts
The Business Requirement
Consider a simple form.
If the answer is Yes, the fare is required.
If the answer is No, there should be no fare.
A straightforward type definition might look like this:
type BusOption = {
busUsage: boolean
busFare?: number
}
At first glance, this seems reasonable.
However, this type allows an impossible state:
const bad: BusOption = {
busUsage: false,
busFare: 500
}
TypeScript accepts this.
But from a business logic perspective, this state is invalid.
If the user did not use the bus, there should not be a fare.
This is where the distinction between optional properties and undefined becomes important.
?: number vs number | undefined
Although these types look similar, they mean different things.
busFare?: number
This means:
the property itself may not exist
Valid examples:
{}
{
busFare: 500
}
This pattern is useful when the property is genuinely optional.
For example:
- API responses
- optional configuration values
- fields that may not exist
busFare: number | undefined
This means:
the property must exist, but the value may be
undefined
Valid:
{
busFare: undefined
}
Invalid:
{}
This pattern often makes sense in forms.
For example:
- the page just loaded
- the user has not entered anything
- the value is not decided yet
A useful mental model is:
-
undefined→ not decided yet -
null→ intentionally empty
However, another problem appears when sending data to the server.
The Hidden JSON.stringify() Problem
Consider this example:
JSON.stringify({
busFare: undefined
})
The expected result might look like this:
{
"busFare": undefined
}
But this is not what happens.
Actual result:
{}
The property disappears completely.
This happens because JSON does not support undefined.
As a result, JSON.stringify() silently removes those fields.
This behavior can easily introduce update bugs.
How Old Data Stayed in the Database
Imagine an existing database record:
{
"busUsage": true,
"busFare": 500
}
Later, the user edits the form and selects:
No, I didn’t use the bus.
The form state becomes:
{
busUsage: false,
busFare: undefined
}
After serialization:
JSON.stringify({
busUsage: false,
busFare: undefined
})
The actual request body becomes:
{
"busUsage": false
}
Notice what is missing:
busFare
Since the backend never receives busFare, the existing value may remain unchanged.
Result:
{
"busFare": 500
}
Even though the user explicitly selected No.
The Type Problem: Invalid Business States
Another issue is that the original type does not express business rules correctly.
This is valid TypeScript:
{
busUsage: false,
busFare: 500
}
But invalid business logic.
The type only describes the shape of the object.
It does not describe valid states.
For this use case, the relationship between fields matters.
A Better Approach: Model Valid States
Instead of optional properties, discriminated unions model the business logic more accurately.
type StoreType =
| {
busUsage: true
busFare: number
}
| {
busUsage: false
busFare: null
}
Now TypeScript understands the dependency between the fields.
If:
busUsage: true
then:
busFare
must exist.
If:
busUsage: false
then:
busFare
must be:
null
This becomes invalid:
{
busUsage: false,
busFare: 500
}
which matches the business requirement.
Why null Worked Better
For temporary form state:
-
undefinedworks well - values may still be undecided
For persistence and API requests:
-
nullis often safer - it explicitly represents intentional emptiness
Unlike undefined, null survives JSON serialization.
JSON.stringify({
busFare: null
})
Result:
{
"busFare": null
}
This allows the backend to explicitly clear values.
Final Thoughts
A useful takeaway is this:
TypeScript types should not only describe object shapes.
They should also describe valid business states.
The difference between:
?:undefinednull
looks small at first.
But when forms, APIs, and persistence are involved, those distinctions can significantly affect application behavior.



Top comments (0)