Nesta aula, vamos mergulhar no universo das datas em JavaScript. Trabalhar com datas é uma das tarefas mais comuns — e, muitas vezes, uma das mais frustrantes — no desenvolvimento web. O objeto Date nativo, embora poderoso, esconde uma série de comportamentos que podem levar a erros sutis, especialmente quando lidamos com fusos horários e formatação. Ao longo desta aula, você entenderá as principais armadilhas do Date, aprenderá a formatar datas de forma eficiente e conhecerá a proposta Temporal, que promete revolucionar a manipulação de datas no futuro da linguagem.

Vamos começar explorando o objeto Date desde sua criação até os métodos mais avançados, passando por exemplos práticos que mostram como evitar erros comuns. Em seguida, veremos como formatar datas para diferentes contextos, usando tanto os métodos nativos quanto bibliotecas populares. Por fim, daremos uma olhada no Temporal, a nova API que está sendo desenvolvida para substituir o Date em aplicações críticas.

Objeto Date e suas dores

O objeto Date é a forma nativa de representar datas e horas em JavaScript. Ele é baseado no tempo Unix (milissegundos desde 1º de janeiro de 1970 UTC) e oferece uma série de métodos para manipulação. No entanto, ele apresenta várias peculiaridades que podem confundir até desenvolvedores experientes.

Uma das principais dores é a questão dos meses: eles são indexados a partir de 0 (janeiro = 0, fevereiro = 1, etc.). Isso é um clássico gerador de bugs. Além disso, o Date é mutável, ou seja, métodos como setDate() alteram o objeto original, o que pode causar efeitos colaterais indesejados. Outra dor é a falta de suporte a fusos horários específicos além do local e UTC, e a formatação nativa é limitada e dependente do ambiente (geralmente baseada no locale do sistema).

Vamos ver um exemplo que ilustra a confusão com os meses:

// Criando uma data para 5 de março de 2025
const data = new Date(2025, 2, 5); // mês 2 = março
console.log(data.toString()); // Wed Mar 05 2025 00:00:00 GMT-0300 (Horário Padrão de Brasília)

// Tentando criar 5 de dezembro, mas errando o índice
const dataErrada = new Date(2025, 12, 5); // mês 12? Isso vai para janeiro de 2026!
console.log(dataErrada.toString()); // Mon Jan 05 2026 00:00:00 GMT-0300

Outro problema comum é a mutabilidade. Observe:

const dataOriginal = new Date(2025, 0, 10); // 10 de janeiro de 2025
const dataCopia = dataOriginal;
dataCopia.setDate(15);
console.log(dataOriginal.getDate()); // 15 – a data original foi alterada!

Para evitar isso, você precisa criar uma cópia explícita com new Date(dataOriginal.getTime()) ou usar structuredClone() (em ambientes modernos).

Além disso, o Date não lida bem com datas inválidas de forma intuitiva. Por exemplo, new Date('2025-13-01') retorna uma data inválida (NaN), mas você só descobre ao chamar getTime() e verificar se é NaN. Isso exige validações manuais.

Essas dores motivaram a criação de bibliotecas como date-fns e Day.js, que fornecem APIs mais amigáveis e imutáveis. Veremos algumas delas na seção de formatação.

Formatação

Formatar datas é uma necessidade constante: exibir uma data em um formato amigável, como "15 de março de 2025", ou em um formato internacional como ISO 8601. O JavaScript nativo oferece algumas ferramentas, mas elas são limitadas.

O método toString() retorna uma representação textual da data, mas o formato varia conforme o ambiente e o fuso horário. Já o método toISOString() retorna uma string ISO 8601 (ex.: "2025-03-15T10:30:00.000Z"), útil para armazenamento e troca de dados, mas não para exibição direta ao usuário.

Para formatação controlada, o método toLocaleDateString() é uma opção nativa que aceita parâmetros de locale e opções. Exemplo:

const data = new Date(2025, 2, 15, 10, 30, 0); // 15 de março de 2025, 10:30
console.log(data.toLocaleDateString('pt-BR')); // 15/03/2025
console.log(data.toLocaleDateString('pt-BR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })); // sábado, 15 de março de 2025
console.log(data.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })); // 10:30

No entanto, para formatos personalizados (como "15/03/2025 10:30") ou para manipulações mais complexas, as bibliotecas são mais convenientes. O date-fns é uma biblioteca moderna, modular e imutável, que oferece funções como format():

import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';

const data = new Date(2025, 2, 15, 10, 30);
console.log(format(data, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })); // 15 de março de 2025
console.log(format(data, 'dd/MM/yyyy HH:mm')); // 15/03/2025 10:30

Outra biblioteca popular é o Day.js, que tem uma API semelhante ao Moment.js, mas com tamanho reduzido. Exemplo:

const dayjs = require('dayjs');
const customParseFormat = require('dayjs/plugin/customParseFormat');

dayjs.extend(customParseFormat);

const data = dayjs('2025-03-15');
console.log(data.format('DD/MM/YYYY')); // 15/03/2025

Essas bibliotecas também resolvem o problema de manipulação (adicionar dias, subtrair meses, etc.) de forma imutável, evitando os efeitos colaterais do Date.

Bibliotecas modernas (Temporal, introdução)

O Temporal é uma proposta para a futura versão do ECMAScript que visa substituir o objeto Date por uma API mais robusta, imutável e com suporte a fusos horários e calendários. Ele está atualmente no estágio 3 do processo TC39, o que significa que já está bem definido e pode ser implementado em breve.

O Temporal fornece vários tipos de objetos: Temporal.Instant para um ponto no tempo (UTC), Temporal.ZonedDateTime para uma data/hora com fuso horário e calendário, Temporal.PlainDate para uma data sem hora, Temporal.PlainTime para hora sem data, e Temporal.Duration para representar durações. Ele também lida com calendários não-gregorianos, algo que o Date não faz.

Um exemplo de uso do Temporal (já que ainda não está amplamente disponível, você pode usar um polyfill ou um ambiente que o suporte, como o Node.js com a flag experimental):

// Criando uma data no fuso de São Paulo
const data = Temporal.ZonedDateTime.from({ timeZone: 'America/Sao_Paulo', year: 2025, month: 3, day: 15, hour: 10, minute: 30 });
console.log(data.toString()); // 2025-03-15T10:30:00-03:00[America/Sao_Paulo]

// Convertendo para outro fuso
const dataEmTóquio = data.withTimeZone('Asia/Tokyo');
console.log(dataEmTóquio.toString()); // 2025-03-15T22:30:00+09:00[Asia/Tokyo]

// Operações aritméticas imutáveis
const dataMaisUmDia = data.add({ days: 1 });
console.log(dataMaisUmDia.toString()); // 2025-03-16T10:30:00-03:00[America/Sao_Paulo]

O Temporal resolve as dores do Date de forma elegante: os objetos são imutáveis, os meses são indexados naturalmente (1-12), e há suporte a fusos horários de forma explícita. Enquanto ele não é oficialmente lançado, você pode usar o polyfill @js-temporal/polyfill ou bibliotecas que já implementam conceitos semelhantes, como Luxon.

O Luxon é uma biblioteca moderna que já adota muitas das ideias do Temporal, como imutabilidade e suporte a fusos horários. Exemplo:

const { DateTime } = require('luxon');
const data = DateTime.fromISO('2025-03-15T10:30', { zone: 'America/Sao_Paulo' });
console.log(data.toFormat('dd/MM/yyyy HH:mm')); // 15/03/2025 10:30
console.log(data.plus({ days: 1 }).toISO()); // 2025-03-16T10:30:00.000-03:00

Com essas ferramentas, você pode trabalhar com datas de forma muito mais segura e produtiva.

Boas práticas e observações finais

Ao trabalhar com datas, algumas boas práticas podem evitar muitos problemas:

  • Sempre que possível, trabalhe com datas em UTC para armazenamento e transmissão, e converta para o fuso local apenas na exibição.
  • Prefira bibliotecas imutáveis (como date-fns ou Day.js) para manipulações, a menos que você esteja confortável com o Date.
  • Valide datas de entrada, especialmente vindas de APIs ou formulários, para evitar valores inválidos.
  • Use o formato ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) para serialização, pois é inequívoco e amplamente aceito.
  • Fique de olho na evolução do Temporal e, se possível, experimente com polyfills para se preparar para o futuro.

Referências

Exercícios

  1. Crie uma função que receba uma string no formato "dd/mm/aaaa" e retorne um objeto Date correto (lembre-se de ajustar o mês). Teste com "29/02/2024" e "31/04/2025".

    ✓ Resposta:
    function parseDataBR(str) {
      const [dia, mes, ano] = str.split('/').map(Number);
      // Mês no Date é 0-indexado, então subtraímos 1
      return new Date(ano, mes - 1, dia);
    }
    console.log(parseDataBR('29/02/2024').toString()); // Thu Feb 29 2024 ...
    console.log(parseDataBR('31/04/2025').toString()); // Invalid Date (pois 31 de abril não existe)
    
  2. Escreva uma função que formate uma data no formato "15 de março de 2025" usando toLocaleDateString com o locale 'pt-BR'.

    ✓ Resposta:
    function formatarData(data) {
      return data.toLocaleDateString('pt-BR', { day: 'numeric', month: 'long', year: 'numeric' });
    }
    console.log(formatarData(new Date(2025, 2, 15))); // 15 de março de 2025
    
  3. Usando a biblioteca date-fns, escreva uma função que receba uma data e retorne o dia da semana por extenso (ex.: "sábado").

    ✓ Resposta:
    import { format } from 'date-fns';
    import { ptBR } from 'date-fns/locale';
    
    function diaDaSemana(data) {
      return format(data, 'EEEE', { locale: ptBR });
    }
    console.log(diaDaSemana(new Date(2025, 2, 15))); // sábado
    
  4. Explique por que new Date(2025, 12, 1) não representa 1º de dezembro de 2025 e qual seria a forma correta.

    ✓ Resposta: Porque o mês é indexado a partir de 0 (janeiro = 0). Portanto, 12 representa janeiro do ano seguinte. A forma correta é new Date(2025, 11, 1) para 1º de dezembro de 2025.
  5. Crie um exemplo de uso do Temporal.PlainDate para adicionar 30 dias a uma data e exibir o resultado em formato ISO. (Se não tiver o Temporal, use um polyfill ou descreva o que faria).

    ✓ Resposta:
    // Usando Temporal (com polyfill)
    const data = Temporal.PlainDate.from('2025-03-15');
    const novaData = data.add({ days: 30 });
    console.log(novaData.toString()); // 2025-04-14