Как правильно указать тип для даты (Date) в 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
Лучшая практика: единый формат в приложении
В профессиональной разработке принято:
- На границе системы (API, база данных) использовать строки ISO 8601
- Внутри приложения работать с объектами
Date - Создать утилитарные функции для конвертации (как
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-типа с валидацией, описанного в первом ответе.