When a UI route consumes or produces URL search/query parameters, treat those parameters as a contract: validate and type them at the route boundary, update them through typed helpers (no as any), and normalize/omit default-derived fields so the resulting query set is consistent.

Apply:

Example (TypeScript):

// Route boundary: validate search
// (Exact API depends on your router, but the idea is: no implicit/unchecked shape)
// validateSearch: (s) => Record<string, unknown>

function updateRequestSearch(
  apply: (draft: Record<string, unknown>) => void,
) {
  const nextSearch = (prev: Record<string, unknown> | undefined) => {
    const draft: Record<string, unknown> = { ...(prev ?? {}) };
    apply(draft);
    return draft;
  };

  navigate({
    search: nextSearch as any, // only if your router typing requires; ideally remove this via proper validateSearch types
    replace: true,
  });
}

function onDateRangeChange(range?: DateTimeRangeValue) {
  updateRequestSearch((draft) => {
    const normalized = range ? normalizeDateTimeRangeValue(range) : undefined;

    if (!normalized || (!normalized.from && !normalized.to)) {
      delete draft.createdAtFrom;
      delete draft.createdAtTo;
      delete draft.createdAtStartTime;
      delete draft.createdAtEndTime;
      return;
    }

    if (normalized.from) draft.createdAtFrom = formatSearchDate(normalized.from);
    else delete draft.createdAtFrom;

    if (normalized.to) draft.createdAtTo = formatSearchDate(normalized.to);
    else delete draft.createdAtTo;

    // Only keep time fields when the corresponding date side exists,
    // and omit defaults (e.g., 00:00:00 / 23:59:59) in your formatter.
    draft.createdAtStartTime = formatSearchTime(normalized.startTime);
    draft.createdAtEndTime = formatSearchTime(normalized.endTime);
  });
}