Avatar ·

Как правильно указать тип для даты (Date) в TypeScript?

Привет! Я только начал изучать TypeScript после JavaScript и столкнулся с непонятной ситуацией при работе с датами. Допустим, у меня есть функция, которая принимает дату рождения пользователя и возвращает его возраст: ```typescript function calculateAge(birthDate) { const today = new Date(); const diff = today.getTime() - birthDate.getTime(); return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25)); } ``` В JavaScript я бы просто передал `new Date('1990-05-15')` и всё работало. Но TypeScript ругается, что параметр `birthDate` имеет неявный тип `any`. Я попробовал написать `birthDate: Date`, но тогда я не могу передать строку `'1990-05-15'` или число `631152000000` (timestamp) — компилятор выдаёт ошибку. Также у меня есть интерфейс пользователя: ```typescript interface User { name: string; createdAt: ???; // что здесь писать? lastLogin: ???; // а здесь? } ``` Я вижу, что `new Date()` возвращает `Date`, но в JSON данные приходят в виде строк ISO 8601 или Unix timestamp. Как правильно типизировать даты в таких случаях? Нужно ли создавать union-тип `Date | string | number`? Или есть какой-то стандартный подход? Подскажите, пожалуйста, как в TypeScript принято работать с датами, чтобы и компилятор не ругался, и код был надёжным? Заранее спасибо! <<>>

Основная проблема: Date — это тип значения, а не формат данных

Вы столкнулись с классической ситуацией. В TypeScript тип Date — это именно объект даты, созданный через new Date(). Он не является строкой или числом. Если ваша функция ожидает Date, вы должны передать именно объект Date, а не строку или timestamp.

Правильное решение: использовать union-тип или фабричную функцию

Самый распространённый и правильный подход — создать тип, который принимает разные представления даты, и внутри функции приводить всё к единому формату. Вот как это делают опытные разработчики:

type DateInput = Date | string | number;

function toDate(input: DateInput): Date {
    if (input instanceof Date) return input;
    if (typeof input === 'string' || typeof input === 'number') {
        const date = new Date(input);
        if (isNaN(date.getTime())) {
            throw new Error(`Invalid date input: ${input}`);
        }
        return date;
    }
    throw new Error(`Unsupported date type: ${typeof input}`);
}

function calculateAge(birthDate: DateInput): number {
    const date = toDate(birthDate);
    const today = new Date();
    const diff = today.getTime() - date.getTime();
    return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
}

// Использование:
calculateAge('1990-05-15');        // строка
calculateAge(631152000000);        // timestamp
calculateAge(new Date('1990-05-15')); // объект Date

Как типизировать интерфейсы с датами

Для интерфейсов, которые работают с данными из JSON (например, из API), лучше использовать строки ISO 8601, а преобразование делать при необходимости:

interface User {
    name: string;
    createdAt: string; // ISO 8601 строка из API
    lastLogin: string; // тоже строка
}

// Когда нужно работать как с датой — преобразуем:
function formatLastLogin(user: User): string {
    const date = new Date(user.lastLogin);
    return date.toLocaleDateString('ru-RU');
}

Почему не стоит использовать union-тип везде?

  • Сложность поддержки: Каждый раз проверять тип вручную — это лишний код и источник ошибок
  • Неоднозначность: Число может быть как timestamp, так и количеством дней
  • Производительность: Лишние проверки в runtime

Лучшая практика: единый формат в приложении

В профессиональной разработке принято:

  1. На границе системы (API, база данных) использовать строки ISO 8601
  2. Внутри приложения работать с объектами Date
  3. Создать утилитарные функции для конвертации (как toDate выше)

Дополнительно: библиотеки для работы с датами

Для сложных проектов рекомендую использовать date-fns или dayjs. Они предоставляют свои типы и утилиты:

import { format, parseISO } from 'date-fns';

const date = parseISO('2023-01-01T12:00:00Z');
console.log(format(date, 'dd.MM.yyyy')); // "01.01.2023"

Главное правило: явно указывайте тип, который ожидаете на входе, и преобразуйте его в Date внутри функции. Это делает код понятным и типобезопасным.

<<>>

Альтернативный подход: Branded types и строгая типизация

Если вы хотите максимальной типобезопасности и не хотите путать обычные строки с датами, можно использовать branded types (таггированные типы):

type ISOString = string & { __brand: 'ISOString' };
type UnixTimestamp = number & { __brand: 'UnixTimestamp' };

function createISOString(s: string): ISOString {
    if (isNaN(Date.parse(s))) {
        throw new Error('Invalid ISO string');
    }
    return s as ISOString;
}

function createTimestamp(n: number): UnixTimestamp {
    if (!Number.isInteger(n) || n < 0) {
        throw new Error('Invalid timestamp');
    }
    return n as UnixTimestamp;
}

interface User {
    createdAt: ISOString;
    lastLogin: UnixTimestamp;
}

// Теперь нельзя случайно передать обычную строку:
const user: User = {
    name: 'Alice',
    createdAt: createISOString('2023-01-01T12:00:00Z'),
    lastLogin: createTimestamp(1672574400)
};

Плюсы:

  • Полная безопасность типов — компилятор не позволит смешать форматы
  • Самодокументируемый код

Минусы:

  • Больше boilerplate кода
  • Нужны дополнительные функции-конструкторы

Этот подход хорошо подходит для крупных проектов с жёсткими требованиями к качеству данных, но для большинства задач достаточно простого union-типа с валидацией, описанного в первом ответе.