O registro de dados cronológicos que precisam permanecer idênticos, independentemente da localização do espectador, como datas de nascimento, alarmes diários, vencimentos de cartões de crédito ou ciclos de faturamento mensal, sempre foi propenso a erros com o objeto Date legado. Como um Date sempre representa um instante específico no tempo e é exibido usando o fuso horário local do espectador, salvar uma data como "1990-01-01" pode fazer com que os usuários a oeste do UTC vejam "1989-12-31", enquanto a modelagem de datas parciais (como uma validade de cartão apenas com o mês) força dias de marcador de posição arbitrários que causam bugs de estouro.
A API Temporal fornece uma família de tipos "simples" que não dependem de local, não têm diferença de fuso horário e representam datas de calendário e horários de relógio exatamente como lidos em um calendário ou relógio:
| Tipo | O que ele modela | Casos de uso típicos |
|---|---|---|
Temporal.PlainDate |
Data completa da agenda (year, month, day) |
Datas de nascimento, datas de eventos, datas de faturamento de assinaturas |
Temporal.PlainTime |
Tempo de relógio (hour, minute, second, ...) |
Alarmes diários (9h, onde quer que o usuário esteja), horário de funcionamento da loja |
Temporal.PlainDateTime |
Data do calendário + hora do relógio sem fuso horário | Programações locais flutuantes, combinando um PlainDate e um PlainTime antes de anexar uma zona |
Temporal.PlainMonthDay |
Mês e dia sem ano | Aniversários, datas comemorativas e feriados fixos recorrentes |
Temporal.PlainYearMonth |
Ano e mês sem dia | Vencimento do cartão de crédito (2027-12), períodos de faturamento mensal |
Como implementar
- Escolha o tipo de
Temporal.Plain*mais específico que corresponda aos seus dados:- Use
Temporal.PlainDate.from()para datas completas do calendário eTemporal.PlainTime.from()para horários do relógio. - Use
Temporal.PlainDateTime.from()(ouplainDate.toPlainDateTime(plainTime)) ao combinar uma data do calendário e um horário do relógio sem vincular a um fuso horário. - Use
Temporal.PlainMonthDay.from()para datas anuais sem um ano e converta em umPlainDatecom.toPlainDate({ year })antes de realizar operações aritméticas de data. - Use
Temporal.PlainYearMonth.from()para valores de ano e mês para que números de dias arbitrários nunca vazem para os cálculos.
- Use
- Observe o parâmetro
calendarao criar tipos simples: ISO 8601 ('iso8601') é o calendário padrão ao analisar strings ISO ou criar comPlainDate.from({ year, month, day }). Ao criarPlainMonthDayouPlainYearMonthde conjuntos de propriedades ou ao trabalhar com calendários não ISO, transmitacalendarexplicitamente (por exemplo,{ month: 10, day: 31, calendar: 'iso8601' }). - Use
.add()com as opçõesoverflowpara intervalos de calendário recorrentes: chamarplainDate.add({ months: 1 })usa como padrão{ overflow: 'constrain' }, fixando datas de fim de mês, como 31 de janeiro a 28/29 de fevereiro. Transmita{ overflow: 'reject' }quando datas de destino inválidas precisarem gerar umRangeErrorem vez de fixar.
Exemplo: datas completas, horários de relógio e 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;
Exemplo: datas parciais (PlainMonthDay e 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;
Exemplo: intervalos mensais recorrentes (overflow: 'constrain' x '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
}
Exemplo: interoperabilidade com o Date legado
Um Date legado é um carimbo de data/hora (um instante), não uma data do calendário. Armazenar "1990-01-01" como Date força você a escolher algum instante (normalmente meia-noite UTC), e ler isso em um fuso horário a oeste do UTC gera "31/12/1989". Se uma biblioteca ou API ainda exigir um Date, faça a conversão explicitamente usando Temporal.Instant em vez de criar strings ISO manualmente:
// 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
Implementação estratégica e práticas recomendadas
- USE
Temporal.PlainDatepara datas completas do calendário,Temporal.PlainTimepara horários de relógio,Temporal.PlainDateTimepara pares de data e hora locais sem fuso,Temporal.PlainMonthDaypara datas anuais recorrentes eTemporal.PlainYearMonthpara vencimentos ou períodos de faturamento de ano-mês. - NÃO faça operações aritméticas diretamente em
Temporal.PlainMonthDay. Primeiro, converta-o em umTemporal.PlainDateusando.toPlainDate({ year }), porque a duração dos meses varia de acordo com o ano. - USE
.add()emTemporal.PlainDatepara intervalos de calendário recorrentes, usandooverflow: 'constrain'(o padrão) para fixar datas de fim de mês ouoverflow: 'reject'para gerar umaRangeErrorem datas inexistentes. - É RECOMENDÁVEL especificar
calendar(por exemplo,calendar: 'iso8601') ao criar tiposPlainMonthDayou Plain não ISO com base em objetos. - NÃO use tipos simples se precisar representar um momento específico no tempo físico (um instante). Use
Temporal.InstantouTemporal.ZonedDateTimepara registros, carimbos de data/hora de eventos ou programação em vários fusos horários. - NÃO construa objetos
Datede strings ISO criadas manualmente (como`${date}T00:00:00Z`). Produza umDatedoepochMillisecondsde umTemporal.InstantouTemporal.ZonedDateTimee leia um de volta comTemporal.Instant.fromEpochMilliseconds(date.getTime())oudate.toTemporalInstant()para que a suposição de fuso horário seja explícita. - NÃO use
Intl.DateTimeFormat.prototype.formatToParts()para extrair ou comparar valores de ano, mês ou dia. Leia.year,.monthe.daydiretamente de um tipo simples e compare com.equals(). - IDENTIFIQUE os fusos horários pelo nome da IANA (por exemplo,
America/New_York) e nunca por um ajuste de tempo fixo do UTC. As compensações mudam com o horário de verão e a legislação, então rótulos como"UTC-5"ficam errados em parte do ano. - NÃO se esqueça de que os objetos
Temporalsão imutáveis. Métodos como.add(),.subtract()ou.with()sempre retornam uma nova instância com uma nova referência, em vez de modificar o objeto original no local, o que os torna seguros para armazenamento direto no estado de IU reativo.
Estratégia de fallback
Substitutos e suporte do navegador para Temporal
Para navegadores que ainda não são compatíveis com a API Temporal nativa, use a detecção de recursos e carregue dinamicamente temporal-polyfill (~15 KB min+gz).
A importação de temporal-polyfill não atribui automaticamente o objeto Temporal ao escopo global. Atribua-o explicitamente se o código depender de 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();
})();