Создание типобезопасных оберток

Типобезопасные обёртки позволяют добавить к timeago.js строгие ограничения TypeScript, которых нет в базовом API: ограниченный набор локалей, гарантированно валидные даты, конкретизированные возвращаемые типы.


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

import { format, register } from 'timeago.js';

type SupportedLocale = 'ru' | 'en_US' | 'de' | 'fr' | 'es' | 'zh_CN' | 'ja' | 'ko';

type DateLike = Date | string | number;

function safeFormat(date: DateLike, locale: SupportedLocale = 'ru'): string {
  return format(date, locale);
}

export { safeFormat };

Теперь TypeScript не пропустит format(date, 'invalid_locale').


Обёртка с валидацией входных данных

import { format } from 'timeago.js';

class TimeagoError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TimeagoError';
  }
}

function validatedFormat(
  date: Date | string | number | null | undefined,
  locale = 'ru'
): string {
  if (date == null) {
    throw new TimeagoError('date cannot be null or undefined');
  }

  const d = new Date(date as any);

  if (isNaN(d.getTime())) {
    throw new TimeagoError(`Invalid date: ${date}`);
  }

  return format(d, locale);
}

Обёртка с Optional возвратом

import { format } from 'timeago.js';

function maybeFormat(
  date: Date | string | number | null | undefined,
  locale = 'ru'
): string | null {
  if (date == null) return null;

  const d = new Date(date as any);
  if (isNaN(d.getTime())) return null;

  return format(d, locale);
}

Обёртка с кастомным типом даты

import { format } from 'timeago.js';

type ISOString = string & { readonly _brand: 'ISOString' };

function toISOString(date: Date): ISOString {
  return date.toISOString() as ISOString;
}

function formatISO(isoDate: ISOString, locale = 'ru'): string {
  return format(isoDate, locale);
}

Брендированный тип не принимает произвольные строки — только те, которые прошли toISOString.


Типобезопасный регистратор локалей

import { register } from 'timeago.js';

type LocaleIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14;

type TypedLocaleFunc = (number: number, index: LocaleIndex) => [string, string];

function typedRegister(locale: string, fn: TypedLocaleFunc): void {
  register(locale, fn as any);
}

Обёртка с результатом Result<T, E>

import { format } from 'timeago.js';

type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

function tryFormat(
  date: unknown,
  locale = 'ru'
): Result<string, string> {
  if (date == null || date === '') {
    return { ok: false, error: 'Empty date' };
  }

  const d = new Date(date as any);

  if (isNaN(d.getTime())) {
    return { ok: false, error: `Invalid date: ${date}` };
  }

  return { ok: true, value: format(d, locale) };
}

// Использование
const result = tryFormat(post.createdAt, 'ru');

if (result.ok) {
  console.log(result.value); // string
} else {
  console.error(result.error);
}

Типобезопасный enrichment

import { format } from 'timeago.js';

interface WithDate {
  createdAt: string;
}

interface WithTimeAgo {
  timeAgo: string;
}

function withTimeAgo<T extends WithDate>(
  item: T,
  locale = 'ru'
): T & WithTimeAgo {
  return {
    ...item,
    timeAgo: format(item.createdAt, locale),
  };
}

function mapWithTimeAgo<T extends WithDate>(
  items: T[],
  locale = 'ru'
): (T & WithTimeAgo)[] {
  return items.map(item => withTimeAgo(item, locale));
}

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

import { render, cancel } from 'timeago.js';

type CleanupFn = () => void;

function typedRender(
  el: HTMLTimeElement,
  locale = 'ru'
): CleanupFn {
  render(el, locale);
  return () => cancel(el);
}

// Использование
const cleanup = typedRender(document.querySelector('time')!, 'ru');

// При размонтировании
cleanup();

Утилита для TypeScript-проектов

// src/lib/timeago.ts
import { format, register, render, cancel } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';

register('ru', ru);

export type Locale = 'ru' | 'en_US';

export const ago = (date: Date | string | number, locale: Locale = 'ru'): string =>
  format(date, locale);

export const renderTime = (el: HTMLTimeElement, locale: Locale = 'ru'): (() => void) => {
  render(el, locale);
  return () => cancel(el);
};