};
}
if (typeof input === 'string') {
const converted = Number(input);
return Number.isFinite(converted) ? { value: converted, valid: true } : { value: fallback, valid: false };
}
return { value: fallback, valid: false };
}
function assertSafeInteger(value: number): number {
if (!Number.isSafeInteger(value)) {
throw new RangeError(Value ${value} exceeds safe integer bounds.);
}
return value;
}
**Architecture Rationale:**
- `Number()` is used instead of `parseInt()` because it fails fast on non-numeric suffixes (`"12px"` β `NaN`).
- `Number.isFinite()` filters out `NaN`, `Infinity`, and `-Infinity` in a single check.
- `Number.isSafeInteger()` explicitly guards against IEEE 754 precision loss beyond `2^53 - 1`.
- Returning a result object (`{ value, valid }`) prevents silent fallbacks and forces callers to handle invalid states explicitly.
### Step 2: Precision & Arithmetic Contracts
Floating-point arithmetic requires tolerance-based comparisons and explicit scaling for decimal-sensitive domains.
```typescript
const DEFAULT_EPSILON = 1e-10;
function compareWithTolerance(a: number, b: number, epsilon: number = DEFAULT_EPSILON): boolean {
return Math.abs(a - b) < epsilon;
}
function scaleToInteger(decimal: number, precision: number = 2): number {
const multiplier = 10 ** precision;
return Math.round(decimal * multiplier);
}
function formatDisplayValue(cents: number, precision: number = 2): string {
return (cents / 10 ** precision).toFixed(precision);
}
Architecture Rationale:
- Direct equality checks (
===) on floats are mathematically unsound. Epsilon comparison abstracts the tolerance threshold.
- Financial calculations should never operate on raw decimals. Scaling to integers (cents) eliminates precision drift entirely.
Math.round() is preferred over Math.floor() for scaling because it correctly handles rounding half-up behavior expected in currency.
- Display formatting is decoupled from calculation logic to prevent accidental type coercion during arithmetic.
Step 3: Deterministic Ranges & Interpolation
UI state, animations, and data normalization frequently require value mapping. These operations must be deterministic and boundary-safe.
function clampValue(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
function interpolateLinear(start: number, end: number, progress: number): number {
const clampedProgress = clampValue(progress, 0, 1);
return start + (end - start) * clampedProgress;
}
function mapRange(value: number, inputMin: number, inputMax: number, outputMin: number, outputMax: number): number {
if (inputMax === inputMin) throw new Error('Input range cannot be zero.');
const normalized = (value - inputMin) / (inputMax - inputMin);
return outputMin + normalized * (outputMax - outputMin);
}
function roundToPrecision(value: number, decimals: number = 2): number {
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
}
Architecture Rationale:
clampValue prevents out-of-bounds propagation in animations and form inputs.
interpolateLinear enforces progress within [0, 1] to prevent extrapolation bugs.
mapRange includes a zero-range guard to avoid NaN division.
roundToPrecision uses multiplication/division rather than string manipulation for performance and type consistency.
Step 4: Internationalized Rendering
User-facing numbers must respect locale conventions without sacrificing performance. Intl APIs are powerful but expensive if instantiated repeatedly.
class NumberFormatter {
private currencyFormat: Intl.NumberFormat;
private compactFormat: Intl.NumberFormat;
private percentFormat: Intl.NumberFormat;
constructor(locale: string = 'en-US') {
this.currencyFormat = new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' });
this.compactFormat = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
this.percentFormat = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 });
}
formatCurrency(value: number): string {
return this.currencyFormat.format(value);
}
formatCompact(value: number): string {
return this.compactFormat.format(value);
}
formatPercentage(value: number): string {
return this.percentFormat.format(value / 100);
}
}
Architecture Rationale:
Intl.NumberFormat instances are cached in a class to avoid repeated allocation overhead.
- Percentage formatting divides by 100 because
Intl expects decimal representation (0.85 β 85%).
- Locale is injected at instantiation, enabling server-side rendering and user preference switching without global state mutation.
Pitfall Guide
1. The parseInt Silent Truncation Trap
Explanation: parseInt() reads until it hits a non-numeric character and returns whatever it parsed. "12px" becomes 12, "abc" becomes NaN, and "0xFF" becomes 255. This permissiveness masks malformed input.
Fix: Replace with Number() or parseFloat() combined with Number.isFinite(). Validate string content with regex if strict formatting is required.
2. typeof NaN Type Confusion
Explanation: typeof NaN returns 'number'. Relying on typeof for validation allows NaN and Infinity to pass through type checks, corrupting downstream calculations.
Fix: Always use Number.isFinite() or Number.isNaN() for validation. Never trust typeof for numeric safety.
3. Bitwise 32-Bit Overflow
Explanation: Bitwise operators (~~, | 0, >> 0) convert operands to signed 32-bit integers. Values exceeding 2147483647 wrap around to negative numbers. This is a silent data corruption vector.
Fix: Use Math.trunc() for truncation. Reserve bitwise operations exclusively for flag manipulation, not numeric conversion.
4. Floating-Point Equality Assumption
Explanation: 0.1 + 0.2 === 0.3 evaluates to false due to binary floating-point representation. Direct equality checks fail unpredictably in calculations involving decimals.
Fix: Implement epsilon-based comparison for all float equality checks. Use integer scaling for financial domains.
5. Array Mutation in Randomization
Explanation: In-place shuffling or random selection that mutates the original array causes state drift in React/Vue components or shared data stores.
Fix: Always copy the array before shuffling ([...arr]). Return new references to maintain immutability contracts.
Explanation: toLocaleString() output varies by environment. "1,234.56" in en-US becomes "1.234,56" in de-DE. Parsing formatted strings back to numbers without locale awareness causes NaN.
Fix: Never parse formatted output. Keep raw numbers in state, format only at the presentation layer, and inject explicit locale codes.
7. Ignoring Safe Integer Boundaries
Explanation: JavaScript loses precision beyond 9007199254740991. Incrementing this value yields the same number. Timestamps, pagination cursors, and hash IDs frequently breach this limit.
Fix: Use Number.isSafeInteger() for validation. For values exceeding bounds, switch to BigInt or string-based identifiers.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Financial Calculations | Integer scaling (cents) + Intl formatting | Eliminates IEEE 754 drift entirely | Low (minimal CPU overhead) |
| UI Animations / Progress | clampValue + interpolateLinear | Prevents out-of-bounds state and extrapolation bugs | Negligible |
| Data Ingestion / APIs | ingestNumeric + assertSafeInteger | Fails fast on malformed payloads, prevents silent corruption | Low (validation cost) |
| High-Frequency Randomization | Fisher-Yates on copied array + Math.random() | Maintains immutability and statistical uniformity | Low (memory copy cost) |
| Legacy Code Migration | Gradual replacement with Number() + isFinite wrappers | Avoids breaking changes while enforcing strictness | Medium (refactoring effort) |
Configuration Template
// numerical-contract.ts
export interface NumericContract {
ingest(input: unknown, fallback?: number): { value: number; valid: boolean };
assertSafe(value: number): number;
compare(a: number, b: number, epsilon?: number): boolean;
scale(decimal: number, precision?: number): number;
formatCurrency(value: number): string;
formatCompact(value: number): string;
formatPercentage(value: number): string;
}
export class StandardNumericContract implements NumericContract {
private formatter: Intl.NumberFormat;
private compactFormatter: Intl.NumberFormat;
private percentFormatter: Intl.NumberFormat;
constructor(locale: string = 'en-US') {
this.formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' });
this.compactFormatter = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
this.percentFormatter = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 });
}
ingest(input: unknown, fallback: number = 0): { value: number; valid: boolean } {
const num = typeof input === 'string' ? Number(input) : (typeof input === 'number' ? input : NaN);
return Number.isFinite(num) ? { value: num, valid: true } : { value: fallback, valid: false };
}
assertSafe(value: number): number {
if (!Number.isSafeInteger(value)) throw new RangeError('Unsafe integer detected.');
return value;
}
compare(a: number, b: number, epsilon: number = 1e-10): boolean {
return Math.abs(a - b) < epsilon;
}
scale(decimal: number, precision: number = 2): number {
return Math.round(decimal * 10 ** precision);
}
formatCurrency(value: number): string { return this.formatter.format(value); }
formatCompact(value: number): string { return this.compactFormatter.format(value); }
formatPercentage(value: number): string { return this.percentFormatter.format(value / 100); }
}
Quick Start Guide
- Initialize the contract: Import
StandardNumericContract and instantiate it with your target locale. Cache the instance at the application root or dependency injection container.
- Replace ingestion points: Locate all
parseInt, parseFloat, and direct Number() calls. Swap them with contract.ingest() and handle the { valid } flag explicitly.
- Audit arithmetic: Identify floating-point comparisons and financial calculations. Apply
contract.compare() for equality checks and contract.scale() before any monetary math.
- Decouple formatting: Remove inline
toLocaleString() calls from components. Route display values through the cached formatter methods to prevent allocation overhead.
- Validate boundaries: Add
contract.assertSafe() guards around pagination offsets, timestamp conversions, and ID generation. Monitor logs for RangeError exceptions during staging.