Типобезопасные обёртки позволяют добавить к 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);
}
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);
}
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);
}
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();
// 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);
};