Продвинутая работа с датами и временем в 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