Библиотека Date-fns написана на TypeScript и активно использует
дженерики для повышения типобезопасности. Особенно важно это при работе
с кастомными типами дат, наследниками Date, а также при
создании собственных утилит поверх 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
Это становится возможным благодаря дженерикам.
Упрощённо сигнатура многих функций выглядит так:
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
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
Здесь дженерики не требуются, поскольку результат не зависит от типа даты.
Особенно важны дженерики в функциях:
addsubsetstartOfDayendOfMonthaddDaysaddWeeksaddMonthsНапример:
import { addMonths } from 'date-fns'
function shiftMonth<T extends Date>(
date: T,
count: number
): T {
return addMonths(date, count)
}
Оператор 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'
Можно создавать дополнительные ограничения.
Пример:
interface TimestampedDate extends Date {
timestamp: number
}
Функция:
function processDate<T extends TimestampedDate>(
date: T
): T {
console.log(date.timestamp)
return addDays(date, 1)
}
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
}
Пример:
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[]
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)
)
)
}
Все функции сохраняют исходный тип.
В сложных утилитах можно извлекать типы автоматически.
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)
}
Но при необходимости сохранения типа лучше использовать дженерики.
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)
}
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)
)
Date-fns не мутирует исходные объекты.
Это особенно удобно при generic-программировании:
function immutableShift<T extends Date>(
date: T
): T {
return addDays(date, 10)
}
Исходный объект остаётся неизменным.
function withDate<
T extends Date
>(
date: T,
handler: (date: T) => T
): T {
return handler(date)
}
Использование:
const result = withDate(
new Date(),
date => addWeeks(date, 2)
)
function transformDates<T extends Date>(
dates: readonly T[]
): T[] {
return dates.map(
date => addDays(date, 1)
)
}
type DateHandler<T extends Date> =
(date: T) => T
Использование:
const handler: DateHandler<Date> =
date => addDays(date, 5)
Date-fns синхронная библиотека, однако generic-типизация полезна и в асинхронном коде.
async function loadAndShift<T extends Date>(
loader: () => Promise<T>
): Promise<T> {
const date = await loader()
return addDays(date, 1)
}
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)
}
}
Неправильно:
function process(date: Date): Date
Правильно:
function process<T extends Date>(
date: T
): T
Плохо:
return addDays(date, 1) as T
Лучше позволять TypeScript выводить тип автоматически.
Плохо:
function process<T>(date: T)
Правильно:
function process<T extends Date>(date: T)
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
Модуль date-fns/fp особенно хорошо сочетается с
дженериками.
import {
addDays,
startOfMonth
} from 'date-fns/fp'
function process<T extends Date>(
date: T
): T {
return startOfMonth(
addDays(5)(date)
)
}
function mapDates<T extends Date>(
dates: T[],
callback: (date: T) => T
): T[] {
return dates.map(callback)
}
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()
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.
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
)
)
}
Дженерики позволяют сохранять тип независимо от сложности цепочки операций.