DEV Community

Yu Hamada
Yu Hamada

Posted on

Why `undefined` Broke My Form Update Logic in TypeScript

When working with forms in TypeScript, it’s easy to assume these two types are almost identical:

busFare?: number
Enter fullscreen mode Exit fullscreen mode

and

busFare: number | undefined
Enter fullscreen mode Exit fullscreen mode

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 null may be a better choice for persistence
  • how discriminated unions can model business rules more safely

Table of Contents


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
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this seems reasonable.

However, this type allows an impossible state:

const bad: BusOption = {
  busUsage: false,
  busFare: 500
}
Enter fullscreen mode Exit fullscreen mode

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:

{}
Enter fullscreen mode Exit fullscreen mode
{
  busFare: 500
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Invalid:

{}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

The expected result might look like this:

{
  "busFare": undefined
}
Enter fullscreen mode Exit fullscreen mode

But this is not what happens.

Actual result:

{}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Later, the user edits the form and selects:

No, I didn’t use the bus.

The form state becomes:

{
  busUsage: false,
  busFare: undefined
}
Enter fullscreen mode Exit fullscreen mode

After serialization:

JSON.stringify({
  busUsage: false,
  busFare: undefined
})
Enter fullscreen mode Exit fullscreen mode

The actual request body becomes:

{
  "busUsage": false
}
Enter fullscreen mode Exit fullscreen mode

Notice what is missing:

busFare
Enter fullscreen mode Exit fullscreen mode

Since the backend never receives busFare, the existing value may remain unchanged.

Result:

{
  "busFare": 500
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

Now TypeScript understands the dependency between the fields.

If:

busUsage: true
Enter fullscreen mode Exit fullscreen mode

then:

busFare
Enter fullscreen mode Exit fullscreen mode

must exist.

If:

busUsage: false
Enter fullscreen mode Exit fullscreen mode

then:

busFare
Enter fullscreen mode Exit fullscreen mode

must be:

null
Enter fullscreen mode Exit fullscreen mode

This becomes invalid:

{
  busUsage: false,
  busFare: 500
}
Enter fullscreen mode Exit fullscreen mode

which matches the business requirement.


Why null Worked Better

For temporary form state:

  • undefined works well
  • values may still be undecided

For persistence and API requests:

  • null is often safer
  • it explicitly represents intentional emptiness

Unlike undefined, null survives JSON serialization.

JSON.stringify({
  busFare: null
})
Enter fullscreen mode Exit fullscreen mode

Result:

{
  "busFare": null
}
Enter fullscreen mode Exit fullscreen mode

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:

  • ?:
  • undefined
  • null

looks small at first.

But when forms, APIs, and persistence are involved, those distinctions can significantly affect application behavior.

Top comments (0)