Duration — Type-Safe Time
150 B minified + gzipped
Modeling time quantities as raw number primitives introduces silent unit-mismatch bugs (such as
passing seconds to a function expecting milliseconds). Because both values are typed as number,
TypeScript cannot catch the error at compile time.
Duration provides a branded, unit-safe representation of time with explicit constructors
(Duration.seconds, Duration.milliseconds) and conversion helpers.
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 TB
A["Raw Number (1)"] -->|"Duration.seconds(1)"| B["Duration (1000ms, Branded)"]
B -->|"Passed to Task.delay"| C["Safe, Type-Checked Delay"]
A -->|"Passed to Task.delay"| D["Compile Error: Type 'number' is not assignable to 'Duration'"]
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:
Modeling retry backoffs with RetryPolicy
Section titled “Modeling retry backoffs with RetryPolicy”Transient network errors, rate-limited endpoints, and brief database lock contentions often succeed
when retried after a short pause. Rather than scattering ad-hoc retry loops and raw millisecond
multipliers across your services, RetryPolicy (from @pplnd/core) pairs with Duration to define
reusable, declarative backoff schedules.
RetryPolicy offers two constructors:
RetryPolicy.constant({ attempts, delay, when?, onRetry? }): Waits a fixedDurationbetween each retry attempt.RetryPolicy.exponential({ attempts, initial, factor?, jitter?, maxDelay?, when?, onRetry? }): Grows the wait duration exponentially after each failure (defaulting tofactor: 2), with optionalmaxDelaycapping and randomizedjitterto prevent synchronized retry spikes (“thundering herds”) across clients.
Problems it solves
Section titled “Problems it solves”- Eliminating time unit ambiguity in configurations: When configuring HTTP timeouts, debounce
intervals, cache TTLs, or retry backoffs, accepting raw numbers forces developers to guess whether
the function expects seconds or milliseconds. Passing
5instead of5000leads to premature timeout failures in production.Durationmakes time units explicit (Duration.seconds(5),Duration.minutes(10)). - Safe duration arithmetic: Calculating total timeout budgets or subtracting safety margins
using raw numbers risks mixing millisecond and second units.
Durationprovides unit-safe arithmetic (Duration.add,Duration.subtract). - Declarative retry and backoff strategies (
RetryPolicy): Expressing constant or exponential backoff with jitter and maximum delay caps viaRetryPolicyeliminates manual exponentiation math and integrates directly withTask.Result.retryandOp.interpret. - Cache TTL and session expiry policies: Calculating cache eviction deadlines or token validity
spans in security middlewares without manual millisecond multiplier math (
Duration.hours(2)). - Unified time modeling across async utilities: Core asynchronous combinators (such as
Task.timeout,Task.delay, andOprepeat schedules) enforceDurationat the type level, establishing a single consistent time model across the entire application.