Типобезопасные операции

Библиотека date-fns изначально проектировалась как набор независимых чистых функций. В сочетании с TypeScript это позволяет строить строго типизированную систему работы с датами без мутаций и неявных преобразований.

Большинство функций библиотеки принимают аргумент типа:

Date | number

Тип number интерпретируется как Unix timestamp в миллисекундах.

Пример сигнатуры:

addDays(date: Date | number, amount: number): Date

Использование:

import { addDays } from 'date-fns'

const now: Date = new Date()

const result = addDays(now, 5)

TypeScript автоматически выводит возвращаемый тип:

const result: Date

Тип DateArg

Во внутренних типах библиотеки часто используется универсальный тип:

type DateArg<DateType extends Date> = DateType | number | string

Он применяется для поддержки расширенных сценариев и совместимости с пользовательскими типами дат.

Например:

function process(date: DateArg<Date>) {
  return new Date(date)
}

Подобная конструкция особенно полезна при создании собственных обёрток над date-fns.


Проверка nullable-значений

Одна из самых распространённых проблем — передача null или undefined.

Небезопасный вариант:

import { format } from 'date-fns'

const userDate: Date | undefined = undefined

format(userDate, 'yyyy-MM-dd')

TypeScript выдаст ошибку:

Argument of type 'undefined' is not assignable

Корректная проверка:

if (userDate) {
  const formatted = format(userDate, 'yyyy-MM-dd')
}

Альтернативный вариант:

const formatted = userDate
  ? format(userDate, 'yyyy-MM-dd')
  : 'Дата отсутствует'

Строгая типизация пользовательских функций

Date-fns особенно эффективна в сочетании с собственными типизированными утилитами.

Пример:

import { isBefore, isAfter } from 'date-fns'

type DateRange = {
  start: Date
  end: Date
}

function isDateInsideRange(
  date: Date,
  range: DateRange
): boolean {
  return (
    isAfter(date, range.start) &&
    isBefore(date, range.end)
  )
}

Использование:

const range: DateRange = {
  start: new Date(2025, 0, 1),
  end: new Date(2025, 11, 31)
}

const result = isDateInsideRange(
  new Date(),
  range
)

Использование Readonly<Date>

Объекты Date являются изменяемыми. Несмотря на то что date-fns не мутирует входящие значения, обычный JavaScript позволяет изменить дату напрямую.

Пример мутации:

const date = new Date()

date.setHours(0)

Для ограничения подобных операций можно применять Readonly<Date>:

function printDate(date: Readonly<Date>) {
  console.log(date.toISOString())
}

Теперь TypeScript запретит вызовы mutating-методов внутри функции.


Безопасная работа с массивами дат

Типизированные коллекции позволяют избежать смешивания разных форматов.

Корректный вариант:

const dates: Date[] = [
  new Date(),
  new Date(2025, 0, 1)
]

Сортировка:

import { compareAsc } from 'date-fns'

dates.sort(compareAsc)

TypeScript гарантирует, что в массив не попадут строки или произвольные значения.


Типобезопасная фильтрация

Пример фильтрации будущих дат:

import { isFuture } from 'date-fns'

const dates: Date[] = [
  new Date(),
  new Date(2030, 0, 1),
  new Date(2020, 0, 1)
]

const futureDates = dates.filter(isFuture)

Тип результата:

Date[]

Generics и пользовательские типы дат

Некоторые проекты используют расширения Date.

Пример:

class UTCDate extends Date {
  timezone = 'UTC'
}

Date-fns поддерживает generic-типизацию:

function cloneDate<T extends Date>(date: T): T {
  return new date.constructor(date.getTime()) as T
}

Использование:

const utcDate = new UTCDate()

const cloned = cloneDate(utcDate)

Тип cloned будет определён как UTCDate.


Безопасное преобразование строк

Одной из ключевых проблем JavaScript остаётся небезопасный парсинг дат.

Плохой вариант:

new Date('01-02-2025')

Поведение зависит от среды выполнения.

Вместо этого используется parse:

import { parse } from 'date-fns'

const parsed = parse(
  '2025-01-15',
  'yyyy-MM-dd',
  new Date()
)

Тип результата:

Date

Проверка валидности даты

Даже объект Date может содержать невалидное значение.

Пример:

const invalid = new Date('wrong')

Для проверки применяется isValid.

import { isValid } from 'date-fns'

if (isValid(invalid)) {
  console.log('Корректная дата')
}

Narrowing типов

TypeScript поддерживает narrowing через пользовательские guards.

Пример:

import { isValid } from 'date-fns'

function isRealDate(value: unknown): value is Date {
  return value instanceof Date && isValid(value)
}

Использование:

const value: unknown = new Date()

if (isRealDate(value)) {
  value.getFullYear()
}

После проверки тип автоматически сужается до Date.


Работа с union-типами

Часто API возвращает дату в нескольких форматах.

Пример:

type InputDate = string | number | Date

Типобезопасное преобразование:

function normalizeDate(input: InputDate): Date {
  if (input instanceof Date) {
    return input
  }

  return new Date(input)
}

Типизация диапазонов времени

Date-fns хорошо сочетается со строгими доменными моделями.

Пример структуры периода:

type Period = {
  readonly start: Date
  readonly end: Date
}

Вычисление длительности:

import { differenceInDays } from 'date-fns'

function getPeriodLength(period: Period): number {
  return differenceInDays(
    period.end,
    period.start
  )
}

Immutable-подход

Все функции date-fns возвращают новый объект даты.

Пример:

import { addMonths } from 'date-fns'

const original = new Date(2025, 0, 1)

const updated = addMonths(original, 1)

Исходная дата не изменяется:

console.log(original)
console.log(updated)

Это особенно важно в Redux, Zustand, MobX и других state-oriented архитектурах.


Типизация форматирования

Функция format принимает строку шаблона:

format(date, 'yyyy-MM-dd')

Однако TypeScript не валидирует шаблон автоматически.

Для повышения безопасности можно использовать string literal types:

type DateFormat =
  | 'yyyy-MM-dd'
  | 'dd.MM.yyyy'
  | 'HH:mm:ss'

Пример:

function safeFormat(
  date: Date,
  pattern: DateFormat
) {
  return format(date, pattern)
}

Теперь использование неверного шаблона вызовет ошибку компиляции.


Enum для шаблонов дат

В крупных проектах часто применяется enum-подход.

enum DateFormats {
  ISO = 'yyyy-MM-dd',
  FULL = 'dd.MM.yyyy',
  TIME = 'HH:mm:ss'
}

Использование:

format(new Date(), DateFormats.ISO)

Типобезопасная сериализация

При передаче данных по сети объекты Date преобразуются в строки.

Тип DTO:

type UserDto = {
  createdAt: string
}

Внутренний тип приложения:

type User = {
  createdAt: Date
}

Преобразование:

function mapUser(dto: UserDto): User {
  return {
    createdAt: new Date(dto.createdAt)
  }
}

Работа с ISO-строками

Date-fns содержит отдельную функцию для ISO-формата:

import { parseISO } from 'date-fns'

const date = parseISO(
  '2025-01-10T12:00:00.000Z'
)

Такой подход безопаснее обычного конструктора Date.


Типизация асинхронных операций

При работе с API особенно важно явно разделять типы.

Пример:

type ApiResponse = {
  updatedAt: string
}

Преобразование:

async function loadData(): Promise<Date> {
  const response: ApiResponse = await fetchData()

  return parseISO(response.updatedAt)
}

Проверка интервалов

Date-fns предоставляет типобезопасные функции работы с диапазонами.

Пример:

import { isWithinInterval } from 'date-fns'

const result = isWithinInterval(
  new Date(),
  {
    start: new Date(2025, 0, 1),
    end: new Date(2025, 11, 31)
  }
)

Структура interval:

{
  start: Date
  end: Date
}

Композиция функций

Date-fns хорошо подходит для построения функциональных пайплайнов.

Пример:

import {
  addDays,
  startOfMonth,
  format
} from 'date-fns'

const result = format(
  addDays(
    startOfMonth(new Date()),
    10
  ),
  'yyyy-MM-dd'
)

Все промежуточные значения сохраняют строгую типизацию.


Ошибки типизации при timestamp

Date-fns принимает timestamp в миллисекундах.

Ошибка:

addDays(1710000000, 1)

Здесь переданы секунды, а не миллисекунды.

Безопасный вариант:

const timestampMs = 1710000000 * 1000

addDays(timestampMs, 1)

Использование branded types

Для исключения путаницы между секундами и миллисекундами можно применять branded types.

Пример:

type Milliseconds = number & {
  __brand: 'milliseconds'
}

type Seconds = number & {
  __brand: 'seconds'
}

Конвертация:

function toMilliseconds(
  seconds: Seconds
): Milliseconds {
  return (seconds * 1000) as Milliseconds
}

Типобезопасные бизнес-правила

Date-fns удобно использовать в доменной логике.

Пример проверки возраста:

import { differenceInYears } from 'date-fns'

function isAdult(
  birthDate: Date
): boolean {
  return (
    differenceInYears(
      new Date(),
      birthDate
    ) >= 18
  )
}

Работа с strictNullChecks

При включённом strictNullChecks TypeScript предотвращает большинство ошибок, связанных с отсутствующими датами.

Пример:

type Event = {
  startDate?: Date
}

Без проверки:

event.startDate.getTime()

Ошибка компиляции:

Object is possibly 'undefined'

Корректный вариант:

if (event.startDate) {
  event.startDate.getTime()
}

Типизация календарных сущностей

В больших системах полезно выделять отдельные типы.

Пример:

type CalendarEvent = {
  id: string
  title: string
  start: Date
  end: Date
}

Проверка пересечения:

import {
  areIntervalsOverlapping
} from 'date-fns'

function intersects(
  a: CalendarEvent,
  b: CalendarEvent
) {
  return areIntervalsOverlapping(
    {
      start: a.start,
      end: a.end
    },
    {
      start: b.start,
      end: b.end
    }
  )
}

Интеграция с Zod

Часто date-fns используется вместе с runtime-валидацией.

Пример:

import { z } from 'zod'

const schema = z.object({
  createdAt: z.coerce.date()
})

После валидации:

type Result = z.infer<typeof schema>

Тип:

{
  createdAt: Date
}

Интеграция с Prisma

Prisma возвращает поля типа Date.

Пример:

const user = await prisma.user.findFirst()

if (user) {
  format(user.createdAt, 'yyyy-MM-dd')
}

TypeScript автоматически определяет тип поля как Date.


Интеграция с React

Date-fns широко используется в React-приложениях.

Пример:

type Props = {
  createdAt: Date
}

export function PostDate({
  createdAt
}: Props) {
  return (
    <span>
      {format(createdAt, 'dd.MM.yyyy')}
    </span>
  )
}

Типизация props предотвращает передачу строк вместо объектов Date.


Типизация кастомных хуков

Пример:

function useFormattedDate(
  date: Date,
  pattern: string
): string {
  return format(date, pattern)
}

Использование:

const value = useFormattedDate(
  new Date(),
  'yyyy-MM-dd'
)

Изоляция временных зон

Date-fns работает с локальной временной зоной среды выполнения. Для строгой типизации timezone-логики часто вводятся дополнительные типы.

Пример:

type UTCISOString = string & {
  __brand: 'utc'
}

Такой подход помогает отделять UTC-значения от локальных дат.


Типизация конфигурации

Пример конфигурации форматирования:

type FormatConfig = {
  locale: Locale
  pattern: string
}

Использование:

import { ru } from 'date-fns/locale'

const config: FormatConfig = {
  locale: ru,
  pattern: 'dd MMMM yyyy'
}

Безопасная работа с unknown

При обработке внешних данных предпочтительно использовать unknown.

Пример:

function parseUnknownDate(
  value: unknown
): Date | null {
  if (typeof value === 'string') {
    const parsed = new Date(value)

    return isValid(parsed)
      ? parsed
      : null
  }

  return null
}

Такой подход исключает неконтролируемые преобразования типов.