Skip to content

One Value, Two Meanings: Global Scope Is a Choice, Not Null

Adityo Guni Waluyo

Null in a scope filter doing two jobs at once: not chosen yet and choose all. A design note on why an ALL sentinel in the enum is more honest.

TL;DR

The KotaPortal dropdown used null to mean both nothing selected and all units, which created confusing backend logic. Instead an explicit ALL value was added to the BidangScope enum so null only means untouched. This keeps the API contract clear, makes validation reliable, and stops teammates guessing what empty means.

One Value, Two Jobs

While wiring a scope dropdown filter in KotaPortal, I stumbled on a piece of logic that made me stop for a second. The null in the selectedBidang variable was doing two different things at once: marking that the user hadn't picked anything yet, and marking that the user had picked "all units".

A classic pattern: an unofficial value quietly picked up a second job. My first take was that this was a neat shortcut. No specific pick, take everything, right? But the further I pulled the thread toward the backend, the clearer the problem got. I was forcing a value that should mean nothing to carry a very specific meaning.

MDN's own definition says it: null is the intentional absence of any object value, while undefined is the absence of a value [5]. Null is falsy and nullish, so it slides smoothly through nullish coalescing and optional chaining [5]. Precisely because of that nature, pressing it into service as an "all units" marker is a recipe for confusion: the absence of an object suddenly means "take everything".

The Nullable Trap in OpenAPI

It gets messier once an API contract is involved. In the OpenAPI Specification, if you insist on null inside an enum, you must write null explicitly into the enum's value list. Providing the nullable: true property alone is not enough [2].

Easy detail to miss, because your eyes feel done after seeing nullable: true. On top of that, different tooling can treat the nullable-plus-enum combination differently; the one clearly safe move is: if null is a legal value, list it in the enum explicitly. TypeScript string literal unions then give us enum-like behavior with compile-time safety [6]. Strict types, no runtime overhead, and with a generator like openapi-typescript converting the OpenAPI schema into runtime-free TypeScript types [4], you get a solid contract without heavy dependencies in the bundle.

The ALL Sentinel: A Choice, Not an Absence

My principle here is firm: when a placeholder starts carrying two meanings, split them immediately. Absence must keep meaning absence.

So I changed the design. Global scope became a real value the user selects, not a missing one. The ALL sentinel became an uppercase member of the BidangScope enum, in line with the make-every-value-explicit decision I wrote about in the API contract piece. The dropdown now renders it as an explicit, clickable option instead of an empty placeholder leaning on a fallback:

type BidangScope = 'finance' | 'hr' | 'asset' | 'ALL';

interface FilterParams {
  bidang: BidangScope;
}

With this, null went back to its original job: marking that the user hasn't touched the dropdown at all. Once the user picks "All units", the string ALL is what goes to the backend, not null. Server-side ambiguity disappears: the backend no longer has to guess whether null means "not filled in yet" or "give me everything".

The Domino Effect on Statuses and History

This design decision didn't stop at the filter dropdown. The six ticket statuses and the ticket history module in KotaPortal now read from the same enum contract, written in one pass. No more raw strings slipping through to the database; every status and history entry is bound to the same type. To make sure the context travels too, the OpenAPI descriptions spell out each value's purpose and effect, beyond what a machine can infer on its own [3].

When you separate "no choice yet" from "choose all", you're respecting the clarity of the data contract. The app gets easier to debug, schema validation gets more trustworthy, and teammates stop guessing what an empty value means. The test is simple: open one dropdown in your app and ask what value comes out before the user touches anything. If the answer is the same value that means something, you've got two meanings sharing one seat.

## Sources [2] Swagger, OpenAPI 3.0 guide: Enums [3] OpenAPI Learn: Providing Documentation and Examples [4] openapi-typescript, README (repo openapi-ts/openapi-typescript) [5] MDN Web Docs: null [6] TypeScript Handbook: Literal Types

Related articles