Даты в TypeScript: как правильно объявить тип и не сойти с ума

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

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

Начать курс

Почему «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                

Блоги

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