A glossary of intuitive, universally unambiguous time, date, and duration domain literals.
declare a universally unambiguous serialization format for dates, times, and datetimes via strict ISO 8601 types with runtime validation.
the core insight: time types are either stamps or floats
- stamp = absolute instant in history (anchored to a specific point in time)
- float = detached time pattern (unanchored, could refer to many instants)
etymology:
Stamp— matches PostgreSQLtimestampsemantics; a time "stamped" into historyFloat— a time that floats, not anchored; could land on many different instants
usecases:
- clarify the format a date string should be in (
const since: IsoDateStamp = '2024-12-15') - cast inputs to strict iso format (
const birthday: IsoDateStamp = asIsoDateStamp(new Date())) - narrow type of strings with runtime validation (
if (!isIsoDateStamp(since)) throw new Error('wrong format'))
stamps (absolute instants)
IsoTimeStamp=yyyy-MM-ddTHH:mm:ssZ(e.g.,2024-06-15T14:30:00Z)IsoDateStamp=yyyy-MM-dd(e.g.,2024-06-15)IsoMonthStamp=yyyy-MM(e.g.,2024-06)IsoYearStamp=yyyy(e.g.,2024)
floats (detached patterns)
IsoTimeFloat=HH:mm:ss(e.g.,14:30:00)IsoHourFloat=HH(e.g.,14)IsoMinuteFloat=mm(e.g.,30)IsoMonthFloat=MM(e.g.,06)IsoDayFloat=dd(e.g.,15)IsoWeekdayFloat=i(e.g.,1for Monday)
ranges (time spans)
IsoTimeStampRange={ since: IsoTimeStamp; until: IsoTimeStamp }IsoDateStampRange={ since: IsoDateStamp; until: IsoDateStamp }
durations (time quantities)
IsoDurationWords= iso 8601 string format with compile-time validation (e.g.,'P1Y','PT30S','P1DT2H')IsoDurationShape= structured object format (e.g.,{ years: 1 },{ seconds: 30 },{ days: 1, hours: 2 })IsoDuration= union of both formats — use whichever is more convenient
npm install iso-timeimport { asIsoTimeStamp, asIsoDateStamp } from 'iso-time';
const timestamp = asIsoTimeStamp(new Date()); // '2024-06-15T14:30:00Z'
const datestamp = asIsoDateStamp(new Date()); // '2024-06-15'import { isIsoTimeStamp, isIsoDateStamp } from 'iso-time';
isIsoTimeStamp('2024-06-15T14:30:00Z'); // true
isIsoTimeStamp('2024-06-15'); // false
isIsoDateStamp('2024-06-15'); // true
isIsoDateStamp('06/15/2024'); // falseimport type { IsoTimeStamp, IsoDateStamp } from 'iso-time';
const occurredAt: IsoTimeStamp = asIsoTimeStamp(new Date());
const birthday: IsoDateStamp = asIsoDateStamp('1990-01-15');import { asIsoHourFloat, asIsoMinuteFloat, asIsoWeekdayFloat } from 'iso-time';
const hour = asIsoHourFloat(new Date()); // '14'
const minute = asIsoMinuteFloat(new Date()); // '30'
const weekday = asIsoWeekdayFloat(new Date()); // '1' (Monday)import { asIsoTimeStampRange, asIsoDateStampRange } from 'iso-time';
const standup = asIsoTimeStampRange({
since: new Date('2024-06-15T10:00:00Z'),
until: new Date('2024-06-15T11:00:00Z'),
});
const vacation = asIsoDateStampRange({
since: new Date('2024-07-01'),
until: new Date('2024-07-14'),
});durations can be expressed in two formats:
- words = iso 8601 string format (
'P1Y','PT30S','P1DT2H30M') - shape = structured object format (
{ years: 1 },{ seconds: 30 })
note
IsoDurationShapeis easier to work with programmatically, so all operations return it by default.IsoDurationWordscan be convenient for inputs but is rarely convenient for manipulation.
import type { IsoDuration, IsoDurationWords, IsoDurationShape } from 'iso-time';
// string format — concise, readable, compile-time validated
const cacheTtl: IsoDurationWords = 'PT5M'; // 5 minutes
const invoicePeriod: IsoDurationWords = 'P1M'; // 1 month
const trialPeriod: IsoDurationWords = 'P14D'; // 14 days
const sessionTimeout: IsoDurationWords = 'PT30M'; // 30 minutes
// object format — easy to manipulate programmatically
const timeout: IsoDurationShape = { seconds: 30 };
const workday: IsoDurationShape = { hours: 8 };
const sprint: IsoDurationShape = { weeks: 2 };
// union type — accepts either format
const flexible: IsoDuration = 'P1D'; // string ✓
const alsoFlexible: IsoDuration = { days: 1 }; // object ✓IsoDurationWords catches invalid iso 8601 strings at compile time:
import type { IsoDurationWords } from 'iso-time';
// ✅ valid — compiles
const good1: IsoDurationWords = 'P1Y';
const good2: IsoDurationWords = 'PT30S';
const good3: IsoDurationWords = 'P1DT2H30M';
// ❌ invalid — compile error
const bad1: IsoDurationWords = '1 day'; // not iso format
const bad2: IsoDurationWords = 'P'; // lacks components
const bad3: IsoDurationWords = 'T30S'; // lacks P prefix
const bad4: IsoDurationWords = { days: 1 }; // wrong type (object)import { toMilliseconds } from 'iso-time';
// both formats work
toMilliseconds({ hours: 1 }); // 3600000
toMilliseconds('PT1H'); // 3600000
toMilliseconds({ days: 1 }); // 86400000
toMilliseconds('P1D'); // 86400000import { addDuration, subDuration, getDuration } from 'iso-time';
const later = addDuration(asIsoTimeStamp(new Date()), { hours: 2 });
const earlier = subDuration(asIsoTimeStamp(new Date()), { days: 1 });
const earlierStill = subDuration(asIsoTimeStamp(new Date()), 'P3D');
const elapsed = getDuration({
of: { range: { since: '2024-06-15T10:00:00Z', until: '2024-06-15T12:30:00Z' } },
}); // { hours: 2, minutes: 30 }import { sumDurations } from 'iso-time';
// spread syntax
const total = sumDurations({ hours: 1 }, { minutes: 30 }, { seconds: 45 });
// { hours: 1, minutes: 30, seconds: 45 }
// array syntax
const durations = [{ hours: 2 }, 'PT30M', { minutes: 15 }];
const combined = sumDurations(durations);
// { hours: 2, minutes: 45 }
// get sum in a specific unit
const totalMinutes = sumDurations(durations, { as: 'minutes' });
// { minutes: 165 }import { sleep } from 'iso-time';
await sleep('PT5S');
await sleep({ seconds: 5 });
await sleep({ milliseconds: 100 });import { now, today } from 'iso-time';
const timestamp = now(); // '2024-06-15T14:30:00Z'
const datestamp = today(); // '2024-06-15'import { startDurationStopwatch } from 'iso-time';
const stopwatch = startDurationStopwatch({ for: 'api call' }, { log: console });
await fetchData();
const { duration } = stopwatch.stop(); // logs elapsed timeimport type {
IsoTimeStamp, IsoDateStamp, IsoTimeFloat,
IsoWeekdayFloat, IsoDuration
} from 'iso-time';
// when exactly the event occurs (absolute)
interface ScheduledEvent {
createdAt: IsoTimeStamp; // '2024-01-15T14:30:00Z'
occursOn: IsoDateStamp; // '2024-03-20'
}
// detached schedule pattern (float)
interface DailySchedule {
since: IsoTimeFloat; // '09:00:00' - every day at 9am
until: IsoTimeFloat; // '10:00:00'
}
// scheduled standup with date range and weekday pattern
interface ScheduledStandup {
repeats: {
since: IsoDateStamp; // '2024-01-15' - start date
until: IsoDateStamp; // '2024-12-31' - end date
};
occurs: {
days: IsoWeekdayFloat[]; // ['1', '3', '5'] - mon, wed, fri
at: IsoTimeFloat; // '09:00:00'
for: IsoDuration; // { minutes: 15 } or 'PT15M'
};
}import type { IsoMonthStamp, IsoDayFloat, IsoMonthFloat } from 'iso-time';
// bill period (absolute)
interface Invoice {
period: IsoMonthStamp; // '2024-01' - January 2024 invoice
}
// bill schedule (float)
interface BillSchedule {
dueOn: IsoDayFloat; // '15' - due on 15th of each month
renewsOn: IsoMonthFloat; // '01' - annual renewal in January
}type-safe rental periods with compile-time validated durations:
import type { IsoDurationWords, IsoDuration, IsoDurationShape, IsoTimeStamp } from 'iso-time';
import { addDuration, getDuration, now } from 'iso-time';
// rental options — iso strings validated at compile time
interface RentalOption {
label: string;
duration: IsoDurationWords;
pricePerDay: number;
}
const excavatorRentals: RentalOption[] = [
{ label: 'half day', duration: 'PT4H', pricePerDay: 150 },
{ label: 'full day', duration: 'P1D', pricePerDay: 250 },
{ label: 'weekend', duration: 'P2D', pricePerDay: 200 },
{ label: 'week', duration: 'P7D', pricePerDay: 175 },
// ❌ compile error — '3 days' is not valid iso 8601
// { label: 'bad', duration: '3 days', pricePerDay: 180 },
];
// rental agreement with typed durations
interface RentalAgreement {
equipment: string;
pickedUpAt: IsoTimeStamp;
duration: IsoDuration; // accepts both formats
gracePeriod: IsoDuration; // 'PT1H' — 1 hour grace
}
// compute when equipment is due back
const computeDueAt = (input: { agreement: RentalAgreement }): IsoTimeStamp => {
const dueAt = addDuration(input.agreement.pickedUpAt, input.agreement.duration);
return addDuration(dueAt, input.agreement.gracePeriod);
};
// check if rental is overdue
const computeOverdueHours = (input: {
agreement: RentalAgreement;
returnedAt: IsoTimeStamp;
}): number => {
const dueAt = computeDueAt({ agreement: input.agreement });
const overdue = getDuration({
of: { range: { since: dueAt, until: input.returnedAt } },
as: 'hours',
});
return overdue.hours;
};
// check how much to refund, minus early return fee
const computeRefundDuration = (input: {
agreement: RentalAgreement;
returnedAt: IsoTimeStamp;
}): IsoDurationShape => {
const dueAt = computeDueAt({ agreement: input.agreement });
const hoursUnused = getDuration({
of: { range: { since: input.returnedAt, until: dueAt } },
as: 'hours',
}).hours;
const hoursRefundable = hoursUnused * 0.8; // 20% early return fee
return { hours: hoursRefundable }; // object format — value is computed
};