Работа с дженериками

Библиотека Date-fns написана на TypeScript и активно использует дженерики для повышения типобезопасности. Особенно важно это при работе с кастомными типами дат, наследниками Date, а также при создании собственных утилит поверх Date-fns.

Дженерики позволяют:

  • сохранять исходный тип даты;
  • писать универсальные функции;
  • избегать потери типов при трансформациях;
  • строить расширяемые API;
  • обеспечивать корректную типизацию цепочек операций.

Базовый принцип типизации Date-fns

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

Date | number

Однако в типах библиотеки используется более сложная модель:

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

Это позволяет функциям работать:

  • с обычным Date;
  • с наследниками Date;
  • с таймстампами;
  • со строками дат.

Многие функции возвращают тот же тип даты, который был передан на вход.

Например:

import { addDays } from 'date-fns'

const date = new Date()

const result = addDays(date, 5)

Тип result:

Date

Но если используется собственный класс даты:

class CustomDate extends Date {}

const custom = new CustomDate()

const result = addDays(custom, 3)

TypeScript сохранит тип:

CustomDate

Это становится возможным благодаря дженерикам.


Как устроены дженерики в типах Date-fns

Упрощённо сигнатура многих функций выглядит так:

function addDays<DateType extends Date>(
  date: DateType,
  amount: number
): DateType

Разбор:

Элемент Назначение
DateType универсальный тип даты
extends Date ограничение типа
date: DateType входной параметр
: DateType возвращается тот же тип

Это критически важно для библиотек и сложных приложений.


Пример без дженериков

Функция без сохранения типа:

function addWeek(date: Date): Date {
  return addDays(date, 7)
}

Проблема:

class UTCDate extends Date {}

const date = new UTCDate()

const result = addWeek(date)

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

Date

Информация о UTCDate потеряна.


Пример с дженериками

Корректная версия:

function addWeek<T extends Date>(date: T): T {
  return addDays(date, 7)
}

Теперь:

class UTCDate extends Date {}

const date = new UTCDate()

const result = addWeek(date)

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

UTCDate

Универсальные утилиты поверх Date-fns

Создание обёртки

import { startOfDay } from 'date-fns'

function normalizeDate<T extends Date>(date: T): T {
  return startOfDay(date)
}

Тип сохраняется автоматически.


Использование нескольких дженериков

Иногда необходимо работать сразу с несколькими типами.

Пример:

function compareDates<
  T extends Date,
  K extends Date
>(
  first: T,
  second: K
): boolean {
  return first.getTime() === second.getTime()
}

Функция принимает разные типы дат:

class UTCDate extends Date {}
class LocalDate extends Date {}

const a = new UTCDate()
const b = new LocalDate()

compareDates(a, b)

Дженерики и функции форматирования

Функции форматирования обычно возвращают строку:

import { format } from 'date-fns'

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

Тип:

string

Здесь дженерики не требуются, поскольку результат не зависит от типа даты.


Дженерики в функциях преобразования

Особенно важны дженерики в функциях:

  • add
  • sub
  • set
  • startOfDay
  • endOfMonth
  • addDays
  • addWeeks
  • addMonths

Например:

import { addMonths } from 'date-fns'

function shiftMonth<T extends Date>(
  date: T,
  count: number
): T {
  return addMonths(date, count)
}

Ограничения через extends

Оператор extends ограничивает допустимые типы.

Пример:

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

Попытка передать строку вызовет ошибку:

cloneDate('2025-01-01')

Ошибка TypeScript:

Argument of type 'string' is not assignable to parameter of type 'Date'

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

Можно создавать дополнительные ограничения.

Пример:

interface TimestampedDate extends Date {
  timestamp: number
}

Функция:

function processDate<T extends TimestampedDate>(
  date: T
): T {
  console.log(date.timestamp)

  return addDays(date, 1)
}

Дженерики и nullable-значения

Date-fns не работает напрямую с null и undefined, поэтому типы приходится расширять вручную.

Пример

function safeAddDays<T extends Date | null>(
  date: T,
  amount: number
): T {
  if (!date) {
    return date
  }

  return addDays(date, amount) as T
}

Дженерики и union-типы

Пример:

type AnyDate = Date | CustomDate

Функция:

function updateDate<T extends AnyDate>(
  date: T
): T {
  return addDays(date, 2)
}

Дженерики в массивах дат

Типизация коллекций

function shiftDates<T extends Date>(
  dates: T[]
): T[] {
  return dates.map(date => addDays(date, 1))
}

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

class UTCDate extends Date {}

const dates: UTCDate[] = [
  new UTCDate(),
  new UTCDate()
]

const result = shiftDates(dates)

Тип:

UTCDate[]

Generic Factory Functions

Date-fns хорошо сочетается с фабричными функциями.

Пример

function createDateProcessor<T extends Date>() {
  return (date: T): T => {
    return addDays(date, 1)
  }
}

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

class UTCDate extends Date {}

const processor =
  createDateProcessor<UTCDate>()

const result =
  processor(new UTCDate())

Дженерики и композиция функций

Date-fns часто используется в функциональном стиле.

Пример композиции

function pipeDate<T extends Date>(
  date: T
): T {
  return endOfMonth(
    startOfDay(
      addDays(date, 5)
    )
  )
}

Все функции сохраняют исходный тип.


Использование infer с Date-fns

В сложных утилитах можно извлекать типы автоматически.

Пример

type ExtractDate<T> =
  T extends Date
    ? T
    : never

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

type A = ExtractDate<Date>
type B = ExtractDate<string>

Результат:

type A = Date
type B = never

Дженерики и перегрузка функций

Иногда необходимо поддерживать разные сигнатуры.

Пример

function parseValue(value: string): Date
function parseValue(value: number): Date

function parseValue(
  value: string | number
): Date {
  return new Date(value)
}

Но при необходимости сохранения типа лучше использовать дженерики.


Продвинутая типизация Date-fns-обёрток

Типобезопасная обёртка

import { addHours } from 'date-fns'

type DateModifier<T extends Date> =
  (date: T) => T

function createHourAdder<T extends Date>(
  hours: number
): DateModifier<T> {
  return (date: T) =>
    addHours(date, hours)
}

Generic Pipelines

Универсальный pipeline

function processPipeline<T extends Date>(
  date: T,
  ...handlers: Array<(date: T) => T>
): T {
  return handlers.reduce(
    (acc, handler) => handler(acc),
    date
  )
}

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

const result = processPipeline(
  new Date(),
  d => addDays(d, 1),
  d => addMonths(d, 2),
  d => startOfDay(d)
)

Дженерики и immutable-подход

Date-fns не мутирует исходные объекты.

Это особенно удобно при generic-программировании:

function immutableShift<T extends Date>(
  date: T
): T {
  return addDays(date, 10)
}

Исходный объект остаётся неизменным.


Создание собственных generic-хелперов

Универсальный helper

function withDate<
  T extends Date
>(
  date: T,
  handler: (date: T) => T
): T {
  return handler(date)
}

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

const result = withDate(
  new Date(),
  date => addWeeks(date, 2)
)

Работа с readonly-массивами

Пример

function transformDates<T extends Date>(
  dates: readonly T[]
): T[] {
  return dates.map(
    date => addDays(date, 1)
  )
}

Generic Utility Types

Создание utility-типа

type DateHandler<T extends Date> =
  (date: T) => T

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

const handler: DateHandler<Date> =
  date => addDays(date, 5)

Дженерики и async-функции

Date-fns синхронная библиотека, однако generic-типизация полезна и в асинхронном коде.

Пример

async function loadAndShift<T extends Date>(
  loader: () => Promise<T>
): Promise<T> {
  const date = await loader()

  return addDays(date, 1)
}

Generic Repository Pattern

Пример архитектурного подхода

interface DateRepository<T extends Date> {
  save(date: T): void
  update(date: T): T
}

Реализация:

class Repository<T extends Date>
  implements DateRepository<T> {

  save(date: T): void {
    console.log(date)
  }

  update(date: T): T {
    return addDays(date, 1)
  }
}

Ошибки при использовании дженериков

Потеря типа через Date

Неправильно:

function process(date: Date): Date

Правильно:

function process<T extends Date>(
  date: T
): T

Избыточное использование as

Плохо:

return addDays(date, 1) as T

Лучше позволять TypeScript выводить тип автоматически.


Слишком широкие ограничения

Плохо:

function process<T>(date: T)

Правильно:

function process<T extends Date>(date: T)

Практический пример: generic-календарь

class UTCDate extends Date {}

class CalendarService<
  T extends Date
> {
  addDay(date: T): T {
    return addDays(date, 1)
  }

  addWeek(date: T): T {
    return addWeeks(date, 1)
  }

  start(date: T): T {
    return startOfMonth(date)
  }
}

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

const service =
  new CalendarService<UTCDate>()

const result =
  service.addWeek(new UTCDate())

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

UTCDate

Интеграция с fp-стилем

Модуль date-fns/fp особенно хорошо сочетается с дженериками.

Пример

import {
  addDays,
  startOfMonth
} from 'date-fns/fp'

function process<T extends Date>(
  date: T
): T {
  return startOfMonth(
    addDays(5)(date)
  )
}

Типизация callback-функций

Пример

function mapDates<T extends Date>(
  dates: T[],
  callback: (date: T) => T
): T[] {
  return dates.map(callback)
}

Дженерики и custom date-классы

Кастомный класс

class BusinessDate extends Date {
  isBusinessDay(): boolean {
    const day = this.getDay()

    return day !== 0 && day !== 6
  }
}

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

const date = new BusinessDate()

const result = addDays(date, 1)

Тип:

BusinessDate

Методы класса сохраняются:

result.isBusinessDay()

Типизация сложных generic-цепочек

Пример

function chain<T extends Date>(
  date: T
): T {
  const a = addDays(date, 1)
  const b = addMonths(a, 2)
  const c = startOfDay(b)

  return c
}

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


Generic Middleware

Пример middleware-подхода

type Middleware<T extends Date> =
  (date: T) => T

function applyMiddleware<T extends Date>(
  date: T,
  middlewares: Middleware<T>[]
): T {
  return middlewares.reduce(
    (acc, middleware) =>
      middleware(acc),
    date
  )
}

Дженерики и декларативный подход

Date-fns особенно эффективен при декларативной обработке дат:

function bookingPipeline<T extends Date>(
  date: T
): T {
  return endOfDay(
    addWeeks(
      startOfWeek(date),
      2
    )
  )
}

Дженерики позволяют сохранять тип независимо от сложности цепочки операций.