Appearance
Dates, Numbers, Internationalization, and Serialization
Business data becomes troublesome where human conventions meet machine representation. A timestamp can be valid but shown on the wrong calendar day. A monetary calculation can be mathematically reasonable but off by one cent. A JavaScript object can look serializable while silently losing values when converted to JSON. These are boundary problems, not merely formatting problems.
A timestamp is not a calendar date
JavaScript's Date represents an instant as milliseconds from the Unix epoch. It does not permanently carry the time zone used to construct or display it:
js
const instant = new Date("2026-03-08T09:30:00Z");
console.log(instant.toISOString());
// 2026-03-08T09:30:00.000ZAn ISO string ending in Z identifies UTC. An explicit offset such as -08:00 also identifies an instant. A date-time string without an offset may be interpreted using rules that are easy to misunderstand, so data exchanged between systems should normally include Z or an offset.
A birthday, billing day, or hotel check-in date is different. The string "2026-03-08" is a calendar date, not necessarily midnight in some implicit zone. Converting it to a Date too early can make it appear as the previous day for a user west of UTC. Preserve date-only values as validated year-month-day data until an actual instant is required.
Parsing should reject ambiguity
Date.parse reliably understands the standardized date-time format, but accepting locale-shaped input such as 03/04/2026 creates an unanswered question: March 4 or April 3?
js
function parseTimestamp(value) {
if (typeof value !== "string") {
throw new TypeError("timestamp must be a string");
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new RangeError("timestamp is invalid");
}
return date;
}This checks whether JavaScript accepted the value; a real protocol should additionally define the exact string shape it permits. Validation and interpretation belong together at the boundary.
Date arithmetic also needs a declared meaning. “Add 24 hours” means elapsed time. “Same local time tomorrow” is calendar arithmetic and may span 23 or 25 hours across a daylight-saving transition. Avoid implementing calendar rules by guessing a millisecond count.
Format for people with Intl
Store and transmit stable data; format it for a person at the presentation boundary:
js
const formatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "America/Los_Angeles",
});
console.log(formatter.format(new Date("2026-07-01T19:00:00Z")));
// Jul 1, 2026, 12:00 PMThe explicit locale controls language and conventions. The explicit IANA time-zone name controls which civil time is displayed. Omitting them deliberately delegates those choices to the user's environment, which is often correct for a personal interface but dangerous for reports whose zone is part of the contract.
Intl.NumberFormat handles grouping, decimal conventions, percentages, currencies, and locale-specific digits:
js
const usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
console.log(usd.format(1234.5)); // $1,234.50Formatting a number as USD does not make the underlying number a correct representation of money. Formatting and arithmetic are separate responsibilities.
Binary floating point has visible edges
JavaScript number uses IEEE 754 binary floating point. Many decimal fractions cannot be represented exactly:
js
console.log(0.1 + 0.2); // 0.30000000000000004Do not repair this with a universal equality tolerance. The acceptable tolerance depends on the domain and scale. For fixed-scale money, store integer minor units when the currency and operation permit it:
js
const subtotalCents = 1999;
const shippingCents = 500;
const totalCents = subtotalCents + shippingCents;
console.log(totalCents); // 2499Integer minor units still require policies for tax, percentage discounts, division, allocation, currencies without two decimal places, and values that exceed the safe-integer range. Use a decimal or money library when those rules are substantial, and round once at a deliberately chosen boundary.
Number.isSafeInteger detects integers that a number can represent without losing unit precision. bigint supports larger integers, but it does not mix implicitly with number and is not serialized by JSON.stringify without an explicit representation.
JSON is a wire format, not an object clone
JSON contains objects, arrays, strings, finite numbers, booleans, and null. It has no native representation for undefined, bigint, Date, Map, Set, functions, symbols, cyclic references, or class identity.
js
const payload = {
createdAt: new Date("2026-07-01T19:00:00Z"),
optional: undefined,
};
console.log(JSON.stringify(payload));
// {"createdAt":"2026-07-01T19:00:00.000Z"}The date became a string through its JSON conversion behavior, while the object property containing undefined disappeared. Parsing the result does not restore the date:
js
const parsed = JSON.parse(JSON.stringify(payload));
console.log(typeof parsed.createdAt); // stringDefine a wire schema instead of treating serialization as automatic preservation:
js
function toInvoiceWire(invoice) {
return {
id: invoice.id,
issuedOn: invoice.issuedOn,
totalCents: invoice.totalCents,
currency: invoice.currency,
};
}On receipt, parse and validate this representation before constructing richer domain values. TypeScript types do not perform that conversion.
For in-memory copying, structuredClone supports more built-in types and cycles than JSON, but it still has defined exclusions and does not preserve arbitrary class behavior. Choose it because its clone semantics match the data, not as a reflexive deep-copy operation.
Design the representation before the formatter
For every temporal or numeric field, ask:
- Is this an instant, a calendar date, a local time, or a recurring rule?
- Which time zone owns calendar interpretation?
- Is this an approximate measurement, an exact integer count, or fixed-scale money?
- Where does rounding occur, and according to which policy?
- What exact representation crosses JSON, storage, or process boundaries?
- Which locale should presentation use?
These decisions prevent more bugs than any individual date or formatting method.
Optional prompts
Predict: In America/Los_Angeles, why can new Date("2026-03-08").toString() display March 7 even though the source text says March 8?
Answer: The standardized date-only string is interpreted as an instant at midnight UTC. Los Angeles is behind UTC, so formatting that instant in local civil time can land on the previous evening. A calendar-only business value should remain an explicitly validated date representation instead of being converted to an instant accidentally.
Explain: Why does Intl.NumberFormat with style: "currency" not make floating-point calculations safe for invoices?
Answer: The formatter controls presentation—currency symbol, grouping, digits, and rounding for display. It does not change binary floating-point storage or define tax, allocation, and rounding policy. Exact fixed-scale domains often use integer minor units or a decimal/money type, then format the final value.
Debug: What information is lost by JSON.parse(JSON.stringify({ createdAt: new Date(), optional: undefined })), and how should a wire contract address it?
Answer: The date becomes a string and does not revive automatically; the
optionalproperty disappears entirely. The wire schema should state the timestamp string format and the meaning of omission versusnull, while the receiving boundary validates and converts the representation deliberately.