Duration — Type-Safe Time
Time is one of the most common dimensions modeled in software systems. We constantly configure request timeouts, token expirations, cache lifetimes, and execution delays.
In standard JavaScript and TypeScript, time is almost universally modeled as a raw number
representing milliseconds. However, a generic number carries no explicit unit information at the
type level. It is incredibly easy to pass a value in seconds to an API that expects milliseconds,
causing timers to fire a thousand times too fast, or connecting clients to time out instantly.
Because both values are structurally just number, TypeScript cannot flag these unit-mismatch
errors. The error only surfaces at runtime — often under specific, intermittent network conditions.
Duration solves this problem by wrapping time quantities in a compile-time type brand. This makes
the intended unit explicit, provides safe mathematical conversions, and prevents raw, un-branded
numbers from being passed to time-sensitive operations.
The problem with primitive numbers for time
Section titled “The problem with primitive numbers for time”Consider a typical background worker that retries failed requests with a customizable timeout and delay:
Nothing prevents a developer from swapping these arguments or passing the wrong scale. The compiler
happily accepts configureTimeout(30, 500), even if the function internally treats both arguments
as milliseconds (making the timeout a practically instant 30 milliseconds).
To fix this defensively, developers often append unit suffixes to variable names (e.g.,
timeoutMs), but this is a convention that relies entirely on human memory and is easily bypassed.
The shift to branded time quantities
Section titled “The shift to branded time quantities”Duration changes this by raising time from a generic primitive to a distinct, branded type. At
runtime, a Duration is represented purely as a standard number of milliseconds, carrying zero
runtime overhead.
At compile time, however, the nominal type brand prevents it from being mixed with generic numbers or other units.
flowchart TD
A["Raw Number (1000)"] -- "Duration.seconds" --> B["Duration (1000ms, Branded)"]
B -- "Passed to Task.delay" --> C["Safe, Type-Checked Delay"]
A -- "Passed to Task.delay" --> D["Compile Error"]
Creating Durations
Section titled “Creating Durations”We construct a Duration by calling the constructor that matches our mental model of the time
quantity. The internal representation is normalized to milliseconds automatically:
Once branded, TypeScript will reject any attempt to pass a raw number where a Duration is
expected:
Converting Durations back to primitives
Section titled “Converting Durations back to primitives”When interfacing with third-party libraries, native browser APIs, or database drivers that require
plain numbers, we unwrap the Duration into the specific unit we need:
Curried time arithmetic
Section titled “Curried time arithmetic”We can perform arithmetic on durations. Duration.add and Duration.subtract are curried,
data-last operations that allow us to adjust time quantities cleanly inside a pipe:
Deep integration with asynchronous APIs
Section titled “Deep integration with asynchronous APIs”Within the pipelined ecosystem, all core time-sensitive operations strictly require Duration
types rather than raw numbers. This guarantees that delays, repeating poll tasks, and timeouts are
safe by default:
When to use Duration
Section titled “When to use Duration”Use Duration when
Section titled “Use Duration when”- You are writing or configuring time-based logic — such as debounces, throttling, retry backoffs, connection timeouts, or cache TTL policies.
- You want to eliminate ambiguous time parameters (e.g.
timeout: number) from your internal APIs and prevent caller-side unit errors. - You are using core
pipelinedtime utilities likeTask.delay,Task.timeout, orOpschedules, which enforceDurationat the compiler level.
Use raw numbers when
Section titled “Use raw numbers when”- You are writing low-level utility functions that interface directly with raw, un-branded system
timestamps (such as
Date.now()). - You are optimizing performance-critical rendering frames or real-time simulation loops where the instantiation of intermediate branded types would introduce garbage collection overhead.