Trabalhar com datas é uma das tarefas mais comuns e, ao mesmo tempo, uma das mais problemáticas em JavaScript. O objeto Date nativo, embora poderoso, carrega consigo décadas de decisões de design que hoje são consideradas confusas e propensas a erros. Nesta aula, vamos explorar o Date a fundo, entender suas limitações, aprender a formatar datas corretamente e vislumbrar o futuro com a nova API Temporal, que está em processo de padronização.

Se você já tentou manipular datas em JavaScript, provavelmente já se deparou com comportamentos estranhos, como meses começando em 0, fuso horário local vs UTC, ou a dificuldade de formatar datas de forma personalizada. Vamos desmistificar esses pontos e fornecer soluções práticas que você pode usar nos seus projetos hoje.

Objeto Date e suas dores

O objeto Date é a única forma nativa de representar datas e horas em JavaScript. Ele é baseado no padrão Unix (milissegundos desde 1 de janeiro de 1970, UTC). Apesar de sua utilidade, ele apresenta várias peculiaridades que podem causar confusão.

Um dos primeiros pontos de atenção é que os meses são indexados a partir de 0 (janeiro = 0, fevereiro = 1, etc.). Isso contrasta com o dia do mês, que começa em 1. Essa inconsistência é fonte de muitos bugs, especialmente para quem está acostumado com outros formatos. Além disso, o objeto Date é mutável, o que significa que métodos como setHours() alteram a data original, podendo causar efeitos colaterais indesejados.

Outro problema é a questão do fuso horário. O Date armazena internamente o instante em UTC, mas a maioria dos métodos (como getHours() e getMinutes()) retornam valores no fuso horário local do sistema. Isso pode gerar inconsistências quando se trabalha com datas em diferentes regiões, exigindo cuidado extra ao serializar ou comparar datas.

Vejamos um exemplo de como criar e manipular datas:

// Criando uma data específica (ano, mês-1, dia, hora, minuto, segundo, ms)
const data = new Date(2025, 0, 15, 10, 30, 0); // 15 de janeiro de 2025, 10:30
console.log(data.toString()); // "Wed Jan 15 2025 10:30:00 GMT-0300 (Horário Padrão de Brasília)"

// Obtendo o mês (0-11)
console.log(data.getMonth()); // 0 (janeiro)

// Obtendo o dia do mês (1-31)
console.log(data.getDate()); // 15

// Obtendo o ano
console.log(data.getFullYear()); // 2025

// Mutabilidade: altera a data original
const outraData = new Date(2025, 0, 15);
outraData.setDate(20); // agora é 20 de janeiro
console.log(outraData.getDate()); // 20

Para mitigar a mutabilidade, é comum criar cópias usando new Date(data.getTime()) ou new Date(data). Também é importante lembrar que, ao comparar datas, a conversão implícita para número pode causar confusão. Por exemplo, data1 == data2 compara as referências, não os valores. Para comparação correta, use getTime() ou operadores de comparação (que convertem para número).

const data1 = new Date(2025, 0, 15);
const data2 = new Date(2025, 0, 15);
console.log(data1 == data2); // false (são objetos diferentes)
console.log(data1.getTime() === data2.getTime()); // true (mesmo instante)
console.log(data1 < data2); // false (comparação numérica)

Outra dor comum é a diferença entre UTC e local. Para evitar problemas, é recomendável usar métodos UTC, como getUTCFullYear(), getUTCMonth(), etc., quando você precisa de consistência entre servidores e clientes.

Formatação

Formatar datas é uma necessidade frequente em aplicações. O JavaScript oferece algumas opções nativas, mas elas são limitadas. O método toString() retorna uma representação completa, mas muitas vezes queremos um formato específico, como dd/mm/aaaa ou dd de mês de aaaa.

O método toLocaleDateString() permite formatar de acordo com o locale, mas a personalização é restrita às opções suportadas. Por exemplo:

const data = new Date(2025, 4, 25); // 25 de maio de 2025
console.log(data.toLocaleDateString('pt-BR')); // "25/05/2025"
console.log(data.toLocaleDateString('pt-BR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })); // "domingo, 25 de maio de 2025"

No entanto, para formatos mais customizados, como 2025-05-25 (ISO), é necessário escrever manualmente:

function formatarISO(data) {
  const ano = data.getFullYear();
  const mes = String(data.getMonth() + 1).padStart(2, '0');
  const dia = String(data.getDate()).padStart(2, '0');
  return `${ano}-${mes}-${dia}`;
}
console.log(formatarISO(data)); // "2025-05-25"

Essa abordagem manual é propensa a erros, especialmente com fuso horário. Uma alternativa é usar toISOString(), que já retorna no formato ISO 8601, mas sempre em UTC:

console.log(data.toISOString()); // "2025-05-25T00:00:00.000Z"

Para aplicações mais complexas, bibliotecas como date-fns ou Moment.js (agora legado) oferecem funções de formatação poderosas. No entanto, para projetos modernos, é recomendável usar Intl.DateTimeFormat, que é nativo e tem bom suporte a locales e opções de formatação.

const formatador = new Intl.DateTimeFormat('pt-BR', {
  day: '2-digit',
  month: '2-digit',
  year: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'America/Sao_Paulo'
});
console.log(formatador.format(data)); // "25/05/2025 21:00" (exemplo)

O Intl também permite formatar datas relativas, como "há 5 dias", usando Intl.RelativeTimeFormat.

Bibliotecas modernas (Temporal, introdução)

Diante das dificuldades do objeto Date, a comunidade e o TC39 (comitê que padroniza o JavaScript) estão desenvolvendo uma nova API chamada Temporal. Ela visa fornecer uma manipulação de datas e horas mais segura, imutável e consistente, resolvendo muitos dos problemas existentes.

O Temporal oferece tipos como Temporal.PlainDate (data sem hora), Temporal.PlainTime (hora sem data), Temporal.PlainDateTime (data e hora sem fuso), Temporal.ZonedDateTime (data e hora com fuso), entre outros. A API é imutável, ou seja, métodos como with() retornam novos objetos, evitando efeitos colaterais.

Um exemplo de uso futuro:

// Exemplo hipotético (Temporal ainda não está disponível em todos os ambientes)
const data = Temporal.PlainDate.from('2025-05-25');
const novaData = data.with({ day: 30 });
console.log(novaData.toString()); // "2025-05-30"

Embora o Temporal ainda esteja em estágio de proposta (Stage 3), já existem polyfills e bibliotecas que implementam parte de sua funcionalidade, como @js-temporal/polyfill. É uma boa ideia acompanhar seu progresso, pois, quando for oficialmente adotado, simplificará muito o trabalho com datas.

Enquanto isso, para projetos sérios, é recomendável usar bibliotecas como date-fns (leve e modular) ou Luxon (da equipe do Moment.js) para manipulação e formatação de datas, pois elas oferecem APIs mais amigáveis e consistentes.

Boas práticas e observações finais

Ao trabalhar com datas em JavaScript, algumas boas práticas podem evitar dores de cabeça:

  • Sempre use UTC para armazenar e transmitir datas (por exemplo, em APIs) e converta para o fuso local apenas para exibição.
  • Evite usar new Date(string) com formatos não padronizados, pois o parsing pode variar entre navegadores.
  • Prefira bibliotecas para manipulação complexa, em vez de reinventar a roda.
  • Considere usar Intl para formatação, pois é nativo e eficiente.
  • Fique atento ao futuro do Temporal e comece a experimentar com polyfills se possível.

Com essas ferramentas e conhecimentos, você estará preparado para lidar com datas de forma robusta em seus projetos.

Referências

Exercícios

  1. Exercício 1: Crie uma função que receba uma data no formato 'AAAA-MM-DD' e retorne o dia da semana por extenso em português (ex.: 'segunda-feira'). Use Intl.DateTimeFormat.
  2. ✓ Resposta:
    function diaDaSemana(dataISO) {
      const data = new Date(dataISO + 'T12:00:00'); // meio-dia para evitar problemas de fuso
      const formatador = new Intl.DateTimeFormat('pt-BR', { weekday: 'long' });
      return formatador.format(data);
    }
    console.log(diaDaSemana('2025-05-25')); // "domingo"
  3. Exercício 2: Escreva uma função que calcule a diferença em dias entre duas datas (ignorando horas). Use Date.UTC para evitar problemas de fuso.
  4. ✓ Resposta:
    function diferencaEmDias(data1, data2) {
      const msPorDia = 24 * 60 * 60 * 1000;
      const utc1 = Date.UTC(data1.getFullYear(), data1.getMonth(), data1.getDate());
      const utc2 = Date.UTC(data2.getFullYear(), data2.getMonth(), data2.getDate());
      return Math.round((utc2 - utc1) / msPorDia);
    }
    console.log(diferencaEmDias(new Date(2025, 4, 25), new Date(2025, 4, 30))); // 5
  5. Exercício 3: Implemente uma função que adicione um número de meses a uma data, preservando o dia do mês (se possível). Por exemplo, 31/01 + 1 mês deve resultar em 28/02 (ou 29/02 em ano bissexto). Use o objeto Date e lide com os casos extremos.
  6. ✓ Resposta:
    function adicionarMeses(data, meses) {
      const novoMes = data.getMonth() + meses;
      const novoAno = data.getFullYear() + Math.floor(novoMes / 12);
      const mesFinal = ((novoMes % 12) + 12) % 12;
      const ultimoDia = new Date(novoAno, mesFinal + 1, 0).getDate();
      const dia = Math.min(data.getDate(), ultimoDia);
      return new Date(novoAno, mesFinal, dia);
    }
    console.log(adicionarMeses(new Date(2025, 0, 31), 1).toString()); // "Fri Feb 28 2025 ..."
  7. Exercício 4: Crie uma função que formate uma data no formato 'dd/mm/aaaa hh:mm' no fuso horário de São Paulo, usando Intl.DateTimeFormat com opções.
  8. ✓ Resposta:
    function formatarDataHora(data) {
      const formatador = new Intl.DateTimeFormat('pt-BR', {
        day: '2-digit',
        month: '2-digit',
        year: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
        timeZone: 'America/Sao_Paulo'
      });
      return formatador.format(data);
    }
    console.log(formatarDataHora(new Date())); // ex.: "25/05/2025 15:30"
  9. Exercício 5: Pesquise sobre a proposta Temporal e escreva um pequeno exemplo de como você usaria Temporal.PlainDate para representar uma data de aniversário e calcular a idade atual (considerando ano bissexto). Use um polyfill se necessário.
  10. ✓ Resposta:
    // Supondo que Temporal esteja disponível (ou use polyfill)
    const aniversario = Temporal.PlainDate.from('1990-05-25');
    const hoje = Temporal.Now.plainDateISO();
    let idade = hoje.year - aniversario.year;
    if (hoje.month < aniversario.month || (hoje.month === aniversario.month && hoje.day < aniversario.day)) {
      idade--;
    }
    console.log(idade);