Запись хронологических данных, которые должны оставаться идентичными независимо от местоположения зрителя, например дат рождения, ежедневных будильников, сроков действия кредитных карт или ежемесячных платежных циклов, исторически была подвержена ошибкам при использовании устаревшего объекта Date. Поскольку Date всегда представляет определенный момент времени и отображается с использованием местного часового пояса зрителя, сохранение даты, например "1990-01-01", может привести к тому, что пользователи к западу от UTC увидят "1989-12-31", в то время как моделирование частичных дат (например, истечение срока действия карты только за месяц) приводит к произвольным дням-заполнителям, которые вызывают ошибки переполнения.
API Temporal предоставляет семейство простых типов, не зависящих от местоположения и не имеющих смещения часового пояса. Они представляют календарные даты и время на часах точно так, как они считываются с календаря или часов:
| Тип | Что моделируется | Примеры использования |
|---|---|---|
Temporal.PlainDate |
Полная календарная дата (year, month, day) |
Даты рождения, даты событий, даты оплаты подписок |
Temporal.PlainTime |
Время по часам (hour, minute, second и т. д.) |
Ежедневные будильники (9:00 по местному времени), часы работы магазинов. |
Temporal.PlainDateTime |
Дата в календаре + время на часах без часового пояса | Плавающие местные расписания, сочетающие PlainDate и PlainTime до прикрепления зоны |
Temporal.PlainMonthDay |
Месяц и день без года | Ежегодные дни рождения, годовщины, праздники с фиксированной датой. |
Temporal.PlainYearMonth |
Год и месяц без дня | Срок действия кредитных карт (2027-12), ежемесячные расчетные периоды |
Как реализовать
- Выберите наиболее узкий тип
Temporal.Plain*, соответствующий вашим данным.- Используйте
Temporal.PlainDate.from()для полных календарных дат иTemporal.PlainTime.from()для времени. - Используйте
Temporal.PlainDateTime.from()(илиplainDate.toPlainDateTime(plainTime)), чтобы объединить календарную дату и время без привязки к часовому поясу. - Используйте
Temporal.PlainMonthDay.from()для ежегодных дат без года и преобразуйте вPlainDateс помощью.toPlainDate({ year })перед выполнением арифметических операций с датами. - Используйте
Temporal.PlainYearMonth.from()для значений года и месяца, чтобы произвольные номера дней не попадали в расчеты.
- Используйте
- Обратите внимание на параметр
calendarпри создании простых типов. ISO 8601 ('iso8601') – это календарь по умолчанию при анализе строк ISO или создании с помощьюPlainDate.from({ year, month, day }). При созданииPlainMonthDayилиPlainYearMonthиз наборов свойств или при работе с календарями, не соответствующими стандарту ISO, явно передавайтеcalendar(например,{ month: 10, day: 31, calendar: 'iso8601' }). - Используйте
.add()с параметрамиoverflowдля повторяющихся календарных интервалов. При вызовеplainDate.add({ months: 1 })по умолчанию используется{ overflow: 'constrain' }, что позволяет зафиксировать даты конца месяца, например 31 января и 28/29 февраля. Передайте{ overflow: 'reject' }, если недопустимые целевые даты должны вызыватьRangeErrorвместо ограничения.
Пример: полные даты, время по часам и PlainDateTime
// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');
// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);
// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"
// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;
Пример: неполные даты (PlainMonthDay и PlainYearMonth)
// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
month: 10,
day: 31,
calendar: 'iso8601',
});
const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());
// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });
// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
year: 2027,
month: 12,
calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;
Пример: повторяющиеся ежемесячные интервалы (overflow: 'constrain' и 'reject')
const billingStart = Temporal.PlainDate.from('2024-01-31');
// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)
// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
// RangeError: day 31 does not exist in February
}
Пример: взаимодействие с устаревшими версиями Date
Устаревший тип Date – это временная метка (момент времени), а не календарная дата. Если вы сохраните "1990-01-01" как Date, вам придется выбрать какой-то момент времени (обычно полночь по UTC), и при чтении в часовом поясе к западу от UTC вы получите "31/12/1989". Если для библиотеки или API по-прежнему требуется Date, выполните явное преобразование с помощью Temporal.Instant, а не создавайте строки ISO вручную:
// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);
// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
.toZonedDateTimeISO('America/New_York')
.toPlainDate();
// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31
Стратегическое внедрение и рекомендации
- РЕКОМЕНДУЕТСЯ использовать
Temporal.PlainDateдля полных календарных дат,Temporal.PlainTimeдля времени,Temporal.PlainDateTimeдля локальных пар дата-время без зоны,Temporal.PlainMonthDayдля повторяющихся ежегодных дат иTemporal.PlainYearMonthдля сроков действия или расчетных периодов в формате год-месяц. - НЕ выполняйте арифметические операции непосредственно с
Temporal.PlainMonthDay. Сначала преобразуйте его вTemporal.PlainDateс помощью.toPlainDate({ year }), поскольку продолжительность месяцев зависит от года. - РЕКОМЕНДУЕТСЯ использовать
.add()вTemporal.PlainDateдля повторяющихся календарных интервалов, полагаясь наoverflow: 'constrain'(по умолчанию) для ограничения дат в конце месяца илиoverflow: 'reject'для выдачиRangeErrorдля несуществующих дат. - РЕКОМЕНДУЕТСЯ указывать
calendar(например,calendar: 'iso8601') при созданииPlainMonthDayили простых типов, не соответствующих стандарту ISO, из объектов. - НЕ ИСПОЛЬЗУЙТЕ простые типы, если вам нужно указать конкретный момент времени. Используйте
Temporal.InstantилиTemporal.ZonedDateTimeдля журналов, временных меток событий или планирования в разных часовых поясах. - НЕ создавайте объекты
Dateиз строк ISO, созданных вручную (например,`${date}T00:00:00Z`). СоздайтеDateизepochMillisecondsTemporal.InstantилиTemporal.ZonedDateTimeи прочитайте его с помощьюTemporal.Instant.fromEpochMilliseconds(date.getTime())илиdate.toTemporalInstant(), чтобы часовой пояс был указан явно. - НЕ ИСПОЛЬЗУЙТЕ
Intl.DateTimeFormat.prototype.formatToParts()для извлечения или сравнения значений года, месяца или дня. Прочитайте.year,.monthи.dayнепосредственно из типа Plain и сравните с.equals(). - РЕКОМЕНДУЕТСЯ указывать часовые пояса по названию в базе данных IANA (например,
America/New_York), а не по фиксированному смещению относительно UTC. Смещение меняется при переходе на летнее время и в соответствии с законодательством, поэтому такие обозначения, как"UTC-5", могут быть неверными в течение части года. - Помните, что объекты
Temporalнеизменяемы. Такие методы, как.add(),.subtract()или.with(), всегда возвращают новый экземпляр с новой ссылкой, а не изменяют исходный объект на месте, что делает их безопасными для хранения непосредственно в реактивном состоянии пользовательского интерфейса.
Резервная стратегия
Резервные варианты и поддержка Temporal в браузерах
Для браузеров, которые пока не поддерживают нативный API Temporal, используйте обнаружение функций и динамически загружайте temporal-polyfill (около 15 КБ min+gz).
Обратите внимание, что импорт temporal-polyfill не приводит к автоматическому назначению объекта Temporal глобальной области видимости. Если ваш код зависит от globalThis.Temporal, назначьте его явно.
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();