Вступление: почему время — это головная боль каждого разработчика
Вы когда-нибудь ловили баг, когда пользователь из Владивостока видит дату завтрашнего дня, хотя на календаре сегодня? Или когда сервер в AWS us-east-1 отправляет время, которое ваш клиент в Калининграде интерпретирует как 3 часа ночи? Добро пожаловать в мир временных зон в TypeScript.
Проблема в том, что JavaScript (и TypeScript как его надстройка) изначально проектировался для браузеров. Встроенный объект Date — это, по сути, замаскированная метка времени в UTC. Он не умеет работать с часовыми поясами так, как нам нужно. И если вы пишете серьёзное приложение на TypeScript — будь то админка на React, бэкенд на Node.js или мобильное приложение на React Native — без грамотной работы с таймзонами вам не обойтись.
В этой статье я покажу вам, как приручить время. Мы разберёмся с базовыми концепциями, научимся использовать современные библиотеки и избежим типичных граблей. Поехали!
Что нужно знать о временных зонах перед тем, как писать код
Прежде чем нырять в TypeScript, давайте расставим точки над i. В мире существует два типа времени: абсолютное (UTC) и локальное (то, что видит пользователь).
- UTC (Coordinated Universal Time) — это эталон. Серверы, базы данных и API должны хранить время именно в UTC. Это железное правило.
- IANA Time Zone Database — это словарь часовых поясов вида
Europe/Moscow,Asia/Vladivostok. Именно эти строки, а не аббревиатуры вроде MSK или PST, нужно использовать в коде. - Offset (смещение) — это разница с UTC, например +03:00. Но помните: смещение может меняться из-за летнего времени (DST). Поэтому хранить offset вместо IANA-зоны — плохая идея.
Золотое правило: храним в UTC, конвертируем на клиенте, используем IANA-строки для идентификации зон.
Почему нативного Date недостаточно: боль и страдания
Давайте посмотрим правде в глаза: new Date() в TypeScript — это как швейцарский нож, у которого сломано лезвие. Он умеет только хранить метку времени и показывать её в локальной зоне браузера или сервера. Попробуйте сделать что-то сложнее:
// Плохой пример: пытаемся работать с зонами нативными средствами
const date = new Date('2024-03-10T12:00:00');
console.log(date.toLocaleString('ru-RU', { timeZone: 'Europe/Moscow' }));
// Выведет: "10.03.2024, 15:00:00" — но это только строка, не объект!
// А если нужно получить Date в другой зоне?
// Упс... нативный JS не умеет создавать Date с привязкой к зоне.
Проблема в том, что Date всегда хранит время в UTC. Все методы вроде getHours() просто преобразуют UTC в локальное время браузера. Вы не можете получить объект Date, который "думает", что он в Нью-Йорке.
Именно поэтому мы используем библиотеки. В TypeScript-экосистеме есть два основных кандидата: Luxon и date-fns-tz.
Luxon: тяжёлая артиллерия для работы с датами
Luxon — это современная библиотека от создателя Moment.js (который уже deprecated). Она написана с нуля, использует нативный Intl.DateTimeFormat и отлично типизирована для TypeScript.
Установка Luxon
npm install luxon
npm install --save-dev @types/luxon
Создание даты в конкретной временной зоне
import { DateTime } from 'luxon';
// Создаём дату в зоне Europe/Moscow
const moscowTime = DateTime.fromObject(
{ year: 2024, month: 3, day: 10, hour: 12 },
{ zone: 'Europe/Moscow' }
);
console.log(moscowTime.toISO());
// Вывод: "2024-03-10T12:00:00.000+03:00"
// Конвертируем в другую зону
const nyTime = moscowTime.setZone('America/New_York');
console.log(nyTime.toISO());
// Вывод: "2024-03-10T05:00:00.000-04:00"
Обратите внимание: DateTime — это иммутабельный объект. setZone() не меняет исходный объект, а возвращает новый.
Парсинг строк с учётом зоны
const parsed = DateTime.fromISO('2024-06-15T14:30:00+05:00', { setZone: true });
console.log(parsed.zoneName); // "Asia/Yekaterinburg" (если зона определима)
console.log(parsed.toFormat('dd.MM.yyyy HH:mm')); // "15.06.2024 14:30"
Получение списка всех доступных зон
import { IANAZone } from 'luxon';
const zones = IANAZone.create('Europe/Moscow');
console.log(zones.isValid); // true
// Все IANA-зоны (работает медленно, используйте с осторожностью)
const allZones = Intl.supportedValuesOf('timeZone');
console.log(allZones.includes('Asia/Vladivostok')); // true
date-fns-tz: лёгкий и модульный подход
Если вы используете date-fns и не хотите тащить тяжёлую библиотеку ради одной функции — date-fns-tz ваш выбор. Это набор утилит, которые расширяют функциональность date-fns для работы с зонами.
Установка
npm install date-fns date-fns-tz
Основные функции
import { format, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
// 1. Конвертация UTC-даты в зону
const utcDate = new Date('2024-07-20T10:00:00Z');
const tokyoDate = utcToZonedTime(utcDate, 'Asia/Tokyo');
console.log(format(tokyoDate, 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: 'Asia/Tokyo' }));
// Вывод: "2024-07-20 19:00:00+09:00"
// 2. Обратная конвертация — из зоны в UTC
const krasnoyarskDate = new Date('2024-07-20T15:00:00+07:00');
const utcAgain = zonedTimeToUtc(krasnoyarskDate, 'Asia/Krasnoyarsk');
console.log(utcAgain.toISOString());
// Вывод: "2024-07-20T08:00:00.000Z"
// 3. Форматирование с учётом зоны
const formatted = format(
utcDate,
'dd.MM.yyyy HH:mm (z)',
{ timeZone: 'Europe/London' }
);
console.log(formatted); // "20.07.2024 11