Несмотря на наличие встроенных типов в date-fns, в
крупных проектах быстро появляются проблемы:
Date, строки и timestamp;Типизированные обёртки позволяют:
Даже при использовании date-fns можно легко написать
опасный код:
import { format } from 'date-fns'
format('2025-01-01' as any, 'yyyy-MM-dd')
TypeScript не всегда способен защитить приложение от неправильных значений, особенно если данные приходят:
В результате появляются:
Invalid time value
или:
RangeError: Invalid time value
Типизированная обёртка позволяет стандартизировать входные данные.
import { format } from 'date-fns'
export function formatDate(
date: Date,
pattern: string = 'yyyy-MM-dd'
): string {
return format(date, pattern)
}
Использование:
formatDate(new Date())
Теперь передача строки станет ошибкой компиляции:
formatDate('2025-01-01')
TypeScript:
Argument of type 'string' is not assignable to parameter of type 'Date'
Одна из самых полезных техник — создание набора допустимых шаблонов.
Без ограничений:
format(date, 'abcxyz')
Ошибка появится только во время выполнения.
type DateFormat =
| 'yyyy-MM-dd'
| 'dd.MM.yyyy'
| 'HH:mm'
| 'dd.MM.yyyy HH:mm'
Теперь создаётся безопасная обёртка:
import { format } from 'date-fns'
export function safeFormat(
date: Date,
pattern: DateFormat
): string {
return format(date, pattern)
}
Использование:
safeFormat(new Date(), 'dd.MM.yyyy')
Ошибка:
safeFormat(new Date(), 'random-format')
Вместо передачи форматов вручную создаются функции с конкретным назначением.
format(date, 'dd.MM.yyyy')
format(date, 'dd.MM.yyyy')
format(date, 'dd.MM.yyyy')
Формат размазан по проекту.
import { format } from 'date-fns'
export function formatShortDate(date: Date): string {
return format(date, 'dd.MM.yyyy')
}
export function formatTime(date: Date): string {
return format(date, 'HH:mm')
}
export function formatApiDate(date: Date): string {
return format(date, 'yyyy-MM-dd')
}
Преимущества:
Во многих проектах даты приходят строками.
const date = 'hello world'
Тип:
string
TypeScript не понимает, что это не дата.
TypeScript позволяет создавать псевдо-уникальные типы.
type DateString = string & {
readonly __brand: 'DateString'
}
import { parseISO, isValid } from 'date-fns'
export function createDateString(
value: string
): DateString {
const parsed = parseISO(value)
if (!isValid(parsed)) {
throw new Error('Invalid date string')
}
return value as DateString
}
Использование:
const date = createDateString('2025-05-01')
Теперь тип:
DateString
parseISO(anyUnknownString)
import { parseISO } from 'date-fns'
export function parseDate(
value: DateString
): Date {
return parseISO(value)
}
Теперь невозможно случайно передать обычную строку.
Работа с API часто подразумевает nullable-поля.
format(user.birthDate, 'yyyy-MM-dd')
Если birthDate === null, приложение упадёт.
import { format } from 'date-fns'
export function formatNullableDate(
date: Date | null | undefined
): string {
if (!date) {
return ''
}
return format(date, 'dd.MM.yyyy')
}
TypeScript позволяет создавать универсальные типизированные функции.
type WithDate = {
createdAt: Date
}
import { compareDesc } from 'date-fns'
export function sortByCreatedAt<T extends WithDate>(
items: T[]
): T[] {
return [...items].sort((a, b) =>
compareDesc(a.createdAt, b.createdAt)
)
}
Использование:
const posts = sortByCreatedAt(postsData)
const comments = sortByCreatedAt(commentsData)
Два независимых параметра:
function getStatistics(start: Date, end: Date)
Неочевидно:
type DateRange = {
start: Date
end: Date
}
import { isAfter } from 'date-fns'
export function createDateRange(
start: Date,
end: Date
): DateRange {
if (isAfter(start, end)) {
throw new Error('Invalid range')
}
return { start, end }
}
import { eachDayOfInterval } from 'date-fns'
export function getRangeDays(
range: DateRange
): Date[] {
return eachDayOfInterval(range)
}
Одна из самых сложных проблем — смешивание локального времени и UTC.
const date = new Date()
Непонятно:
type UTCDate = Date & {
readonly __brand: 'UTCDate'
}
export function createUTCDate(
value: string
): UTCDate {
return new Date(value) as UTCDate
}
import { formatISO } from 'date-fns'
export function formatUTCDate(
date: UTCDate
): string {
return formatISO(date)
}
Теперь нельзя случайно передать локальную дату.
add(date, {
days: 30
})
Непонятно:
import { addDays } from 'date-fns'
export function addTrialPeriod(
date: Date
): Date {
return addDays(date, 14)
}
type TimeUnit =
| 'days'
| 'hours'
| 'minutes'
import { add } from 'date-fns'
type DurationMap = {
days?: number
hours?: number
minutes?: number
}
export function addDuration(
date: Date,
duration: DurationMap
): Date {
return add(date, duration)
}
В больших приложениях даты имеют контекст.
Примеры:
type PaymentDate = Date & {
readonly __brand: 'PaymentDate'
}
type DeliveryDate = Date & {
readonly __brand: 'DeliveryDate'
}
export function createPaymentDate(
value: Date
): PaymentDate {
return value as PaymentDate
}
export function createDeliveryDate(
value: Date
): DeliveryDate {
return value as DeliveryDate
}
function processPayment(date: PaymentDate) {
// ...
}
Теперь невозможно случайно передать:
DeliveryDate
parseISO('invalid')
Вернёт:
Invalid Date
что опасно.
type ParseResult =
| {
success: true
value: Date
}
| {
success: false
error: string
}
import { parseISO, isValid } from 'date-fns'
export function safeParseISO(
value: string
): ParseResult {
const date = parseISO(value)
if (!isValid(date)) {
return {
success: false,
error: 'Invalid ISO date'
}
}
return {
success: true,
value: date
}
}
В крупных приложениях часто создаётся единый слой работы с датами.
shared/
lib/
date/
format.ts
parse.ts
compare.ts
utc.ts
range.ts
export const dateService = {
formatShortDate,
formatTime,
parseDate,
safeParseISO,
addTrialPeriod,
}
Использование:
dateService.formatShortDate(new Date())
type Timezone =
| 'UTC'
| 'Europe/Moscow'
| 'Asia/Almaty'
type ZonedDateOptions = {
timezone: Timezone
}
function formatZonedDate(
date: Date,
options: ZonedDateOptions
)
Такая структура делает timezone обязательным параметром.
При использовании date-fns-tz типизированные обёртки
особенно полезны.
import { formatInTimeZone } from 'date-fns-tz'
export function formatUserDate(
date: Date,
timezone: Timezone
): string {
return formatInTimeZone(
date,
timezone,
'dd.MM.yyyy HH:mm'
)
}
Хотя date-fns уже работает иммутабельно, обёртки
помогают явно фиксировать контракт.
type ImmutableDate = Readonly<Date>
import { addDays } from 'date-fns'
export function addImmutableDays(
date: ImmutableDate,
amount: number
): ImmutableDate {
return addDays(date, amount)
}
import { isBefore, isAfter } from 'date-fns'
export function isExpired(
expiresAt: Date
): boolean {
return isBefore(expiresAt, new Date())
}
export function isFutureDate(
date: Date
): boolean {
return isAfter(date, new Date())
}
Такие функции:
function updateUser(data: any)
type User = {
createdAt: Date
updatedAt: Date
}
function updateUserDates(
user: User
): User {
return {
...user,
updatedAt: new Date(),
}
}
TypeScript не защищает runtime.
Поэтому типизированные обёртки почти всегда комбинируются с:
isValid;zod;io-ts;valibot;export function isDate(
value: unknown
): value is Date {
return (
value instanceof Date &&
!isNaN(value.getTime())
)
}
import { format } from 'date-fns'
export function tryFormatDate(
value: unknown
): string {
if (!isDate(value)) {
return ''
}
return format(value, 'dd.MM.yyyy')
}
Обёртки особенно эффективны при комбинировании.
const date = createDateString(
'2025-05-01'
)
const parsed = parseDate(date)
const formatted =
formatShortDate(parsed)
Каждый этап:
Типизированные обёртки часто используются как адаптер между:
type ApiUser = {
createdAt: string
}
type User = {
createdAt: Date
}
import { parseISO } from 'date-fns'
export function mapUser(
apiUser: ApiUser
): User {
return {
createdAt: parseISO(
apiUser.createdAt
),
}
}
Обёртки уменьшают связанность проекта с библиотекой.
Если позже потребуется:
изменения затронут только слой адаптеров.
Наиболее устойчивый подход:
UI
↓
typed wrappers
↓
date-fns
В таком случае:
date-fns;