Почему «Date» в TypeScript — это ловушка?
Представьте: вы пишете приложение, всё работает идеально. Но в один прекрасный день приходит баг-репорт: «Пользователь из Токио видит дату на день раньше». Вы открываете код и видите классику: let date: Date = "2024-12-01". Или ещё хуже — any. Знакомо?
Вопрос «How do I express a date type in TypeScript?» на Stack Overflow набрал десятки тысяч просмотров. И не зря. Казалось бы, ответ простой: используй Date. Но на практике всё сложнее. TypeScript — это надстройка над JavaScript, а Date в JS — объект, который ведёт себя... своеобразно.
В этой статье мы разберём не просто синтаксис, а правильную типизацию дат, которая спасёт ваш проект от багов, повысит производительность и сделает код понятным для команды. Вы узнаете, как работать с реальными датами в TypeScript, не теряя времени на отладку.
1. Базовая типизация: Date, string или number?
Самый очевидный вариант — встроенный тип Date:
let birthday: Date = new Date('1990-05-15');
Работает? Да. Но есть нюанс: TypeScript не проверяет, валидная ли дата. Вы можете написать new Date('abc') — ошибки компиляции не будет, но в рантайме получите Invalid Date. А попытка вызвать .getTime() вернёт NaN.
Более строгий вариант — использовать строки в формате ISO 8601:
type ISODateString = string;
let eventDate: ISODateString = '2024-12-01T10:00:00Z';
Плюс: простой тип, легко сериализуется в JSON. Минус: никакой валидации — вы можете присвоить 'hello', и TS промолчит.
Третий вариант — timestamp (number):
type Timestamp = number;
let ms: Timestamp = Date.now();
Идеально для математических операций, но для человека нечитаемо. И снова — любое число пройдёт проверку.
Резюме по базовым типам
Если вам нужна быстрая и грязная типизация — используйте Date. Если работаете с API — string. Если только вычисления — number. Но для серьёзных проектов этого мало. Давайте углубимся.
2. Кастомные типы и brand-типизация (branded types)
Чтобы TypeScript не путал строку-дату с обычной строкой, используют branded типы. Это как наклейка «Я — дата, проверено!»:
type DateString = string & { __brand: 'DateString' };
function toDateString(date: Date): DateString {
return date.toISOString() as DateString;
}
function processDate(d: DateString) {
// здесь d точно ISO-строка
}
const iso = toDateString(new Date());
processDate(iso); // ОК
// processDate('hello'); // ❌ Ошибка!
Это добавляет безопасность на этапе компиляции. Цена — немного больше кода. Но для крупных проектов (особенно если у вас микросервисы или REST API) это золотой стандарт.
3. Библиотеки для работы с датами в TypeScript
Сырой Date — зло. Он мутабельный, парсит строки неконсистентно, а таймзоны — отдельная боль. Поэтому в реальных проектах используют библиотеки. Вот три фаворита:
3.1. date-fns (лёгкая и модульная)
import { format, parseISO } from 'date-fns';
const date: Date = parseISO('2024-12-01T10:00:00Z');
const formatted: string = format(date, 'dd.MM.yyyy');
Плюс: tree-shaking, immutable (возвращает новые даты), отличная типизация.
3.2. Day.js (минималистичная)
import dayjs, { Dayjs } from 'dayjs';
const now: Dayjs = dayjs();
const formatted: string = now.format('DD.MM.YYYY');
Day.js — это как Moment.js, но без жирного бандла. Тип Dayjs — это обёртка, которую легко передавать между функциями.
3.3. Luxon (от создателей Moment.js)
import { DateTime } from 'luxon';
const dt: DateTime = DateTime.fromISO('2024-12-01');
console.log(dt.toISODate()); // '2024-12-01'
Luxon — тяжёлая артиллерия. Иммутабельность, мощные таймзоны, строгая типизация. Если ваше приложение «живёт» в разных часовых поясах — это выбор №1.
4. Оптимизация производительности: что делает код медленным?
Теперь к самому интересному — производительность. Когда вы работаете с датами в TypeScript, узкие места часто связаны не с типизацией, а с тем, как вы создаёте и сравниваете объекты.
4.1. Избегайте new Date() в циклах
// ❌ Плохо: создаём Date на каждой итерации
for (let i = 0; i < 10000; i++) {
const d = new Date('2024-01-01');
console.log(d.getTime());
}
// ✅ Хорошо: один раз создаём, много раз используем
const fixedDate = new Date('2024-01-01').getTime();
for (let i = 0; i < 10000; i++) {
console.log(fixedDate);
}
Создание объекта Date — дорогая операция (парсинг строки, аллокация). В высоконагруженных местах лучше работать с числами (timestamp).
4.2. Сравнение дат: используйте числа
const a = new Date('2024-01-01');
const b = new Date('2024-12-01');
// ❌ Плохо: сравнение объектов
if (a > b) { /* ... */ }
// ✅ Хорошо: сравнение чисел
if (a.getTime() > b.getTime()) { /* ... */ }
// ✅ Ещё лучше: сразу хранить timestamp
const aTs = a.getTime();
const bTs = b.getTime();
Оператор > для Date работает, но вызывает неявное преобразование (coercion). Работа с числами — всегда быстрее.
4.3. Типизация для высоконагруженных вычислений
Если вы пишете, например, календарь или аналитику, где тысячи дат — используйте union типы для строгих форматов:
type DatePrecision = 'year' | 'month' | 'day' | 'full';
function formatDate(d: Date, precision: DatePrecision): string {
const ts = d