Временные зоны в TypeScript: как не сойти с ума от дат

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

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

Начать курс

Вступление: почему время — это головная боль каждого разработчика

Вы когда-нибудь ловили баг, когда пользователь из Владивостока видит дату завтрашнего дня, хотя на календаре сегодня? Или когда сервер в 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                

Блоги

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