Продвинутая работа с датами и временем в TypeScript: методы и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Продвинутая работа с датами и временем в TypeScript: от типизации до сложных операций

Работа с датами и временем — одна из самых частых и одновременно коварных задач в веб-разработке. На продвинутом уровне простого использования new Date() уже недостаточно. TypeScript предоставляет мощную систему типов, а современные библиотеки (Luxon, Date-fns) позволяют писать безопасный, предсказуемый и производительный код. В этой статье мы разберем продвинутые техники: строгую типизацию, работу с часовыми поясами, сложное форматирование и парсинг, а также создание собственных утилит.

1. Строгая типизация дат: кастомные типы и Branded Types

Тип Date в TypeScript не защищает от передачи некорректного значения. Например, функция, ожидающая дату рождения, может получить текущую метку времени. Решение — использовать Branded Types (брендированные типы) или Nominal Typing.

// Branded Type для даты рождения
type BirthDate = Date & { __brand: 'BirthDate' };

function createBirthDate(date: Date): BirthDate {
  // Валидация: дата не должна быть в будущем
  if (date.getTime() > Date.now()) {
    throw new Error('Дата рождения не может быть в будущем');
  }
  return date as BirthDate;
}

function calculateAge(birthDate: BirthDate): number {
  const diff = Date.now() - birthDate.getTime();
  return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
}

// Использование
const rawDate = new Date('1990-05-15');
const birth = createBirthDate(rawDate);
console.log(calculateAge(birth)); // OK

// Ошибка: Argument of type 'Date' is not assignable to parameter of type 'BirthDate'
// console.log(calculateAge(new Date())); 

Этот подход гарантирует, что в функцию calculateAge попадет только валидированная дата рождения. Для более сложных сценариев можно создавать типы для временных меток (UnixTimestamp), интервалов (TimeInterval) и т.д.

2. Продвинутое форматирование и парсинг с помощью Intl.DateTimeFormat

Встроенный объект Intl.DateTimeFormat позволяет гибко форматировать даты без сторонних библиотек. На продвинутом уровне мы можем создавать переиспользуемые форматтеры и обрабатывать сложные локали.

// Создание форматтера с опциями
const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  weekday: 'long',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'Europe/Moscow',
});

// Форматирование
console.log(formatter.format(new Date())); 
// "пятница, 25 октября 2024 г., 14:30"

// Парсинг (кастомная утилита)
function parseLocalizedDate(dateStr: string, locale: string): Date | null {
  const parts = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/);
  if (!parts) return null;
  
  const [, year, month, day] = parts;
  return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}

Совет: Для производительности создавайте форматтеры один раз и переиспользуйте их. Избегайте создания нового экземпляра на каждый вызов.

3. Работа с часовыми поясами и библиотекой Luxon

Встроенный Date плохо справляется с часовыми поясами. Luxon — современная библиотека от создателя Moment.js, которая идеально интегрируется с TypeScript.

import { DateTime, Settings } from 'luxon';

// Установка часового пояса по умолчанию
Settings.defaultZone = 'Europe/Moscow';

// Создание даты с явным указанием зоны
const meeting = DateTime.fromISO('2024-11-01T15:00:00', { zone: 'America/New_York' });
console.log(meeting.toISO()); // 2024-11-01T15:00:00.000-04:00

// Конвертация в другой пояс
const moscowTime = meeting.setZone('Europe/Moscow');
console.log(moscowTime.toFormat('dd.MM.yyyy HH:mm')); // 01.11.2024 22:00

// Работа с интервалами
const start = DateTime.now();
const end = start.plus({ hours: 2, minutes: 30 });
const interval = start.until(end);
console.log(interval.length('hours')); // 2.5

// Типизированные вычисления
type DurationUnit = 'years' | 'months' | 'days' | 'hours' | 'minutes';

function addDuration(date: DateTime, amount: number, unit: DurationUnit): DateTime {
  return date.plus({ [unit]: amount });
}

Luxon предоставляет строгую типизацию для всех методов, что делает код безопаснее. Кроме того, библиотека поддерживает работу с нестандартными календарями (исламский, еврейский) через плагины.

4. Оптимизация производительности: мемоизация и debounce для дат

При работе с датами в реальном времени (например, обновление таймера или календаря) важно избегать лишних вычислений. Используем мемоизацию для тяжелых операций и debounce для частых обновлений.

// Мемоизация функции форматирования
function memoize<T, R>(fn: (arg: T) => R): (arg: T) => R {
  const cache = new Map<T, R>();
  return (arg: T) => {
    if (cache.has(arg)) {
      return cache.get(arg)!;
    }
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const memoizedFormat = memoize((date: Date) => {
  // Тяжелая операция форматирования
  return new Intl.DateTimeFormat('ru-RU', {
    dateStyle: 'full',
    timeStyle: 'medium',
  }).format(date);
});

// Debounce для обновления UI
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
  let timeoutId: ReturnType<typeof setTimeout>;
  return ((...args: any[]) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  }) as T;
}

const updateClock = debounce(() => {
  const now = new Date();
  document.getElementById('clock')!.textContent = memoizedFormat(now);
}, 1000);

// Запуск обновления
setInterval(updateClock, 100);

Этот код гарантирует, что форматирование выполняется только для новых значений, а обновление DOM

Блоги

Рекомендаций по книгам