Решение проблем с типами

При интеграции timeago.js в TypeScript-проекты возникает несколько типичных проблем: несоответствие типов, конфликты деклараций, ошибки при строгих режимах компилятора. Большинство решаются стандартными инструментами TypeScript.


Ошибка: тип ‘null’ не совместим с ‘Date | string | number’

import { format } from 'timeago.js';

const date: string | null = getDate(); // может вернуть null

format(date, 'ru'); // Ошибка: Argument of type 'string | null' is not assignable to...

Решение 1 — Non-null assertion (если гарантированно не null):

format(date!, 'ru');

Решение 2 — Условная проверка:

if (date !== null) {
  format(date, 'ru'); // TypeScript знает: date — string
}

Решение 3 — Nullish coalescing:

format(date ?? new Date(), 'ru');

Ошибка: ‘unknown’ не совместим с DateInput

В strict-режиме входные данные могут иметь тип unknown:

function processItem(item: unknown) {
  format(item as any, 'ru'); // Небезопасно
}

// Безопасный вариант
function processItem(item: unknown): string | null {
  if (
    typeof item === 'string' ||
    typeof item === 'number' ||
    item instanceof Date
  ) {
    return format(item, 'ru');
  }
  return null;
}

Ошибка: Cannot find module ‘timeago.js’ or its corresponding type declarations

error TS2307: Cannot find module 'timeago.js' or its corresponding type declarations.

Решение 1 — Установить типы (если пакет поддерживает):

timeago.js включает типы в основной пакет, переустановить:

npm install timeago.js@latest

Решение 2 — Создать локальное объявление типов:

// types/timeago.js.d.ts
declare module 'timeago.js' {
  type DateInput = Date | string | number;

  export function format(date: DateInput, locale?: string): string;
  export function render(nodes: Element | Element[], locale?: string): void;
  export function cancel(nodes?: Element | Element[]): void;
  export function register(locale: string, fn: (n: number, i: number) => [string, string]): void;
}

Ошибка: LocaleFunc не совместим с ожидаемым типом

register('custom', (num, index) => {
  return 'строка'; // Ошибка: нужен [string, string], не string
});

Правильное возвращаемое значение — кортеж:

register('custom', (num, index): [string, string] => {
  const forms: [string, string][] = [
    ['только что', 'через секунду'],
    // ...
  ];
  return forms[index] ?? ['давно', 'скоро'];
});

Ошибка: strictNullChecks с render

const el = document.querySelector('time'); // HTMLElement | null

render(el, 'ru'); // Ошибка: Type 'null' is not assignable

Решение 1 — Проверка перед вызовом:

const el = document.querySelector('time');
if (el) render(el, 'ru');

Решение 2 — Non-null assertion:

render(document.querySelector('time')!, 'ru');

Решение 3 — Явная проверка с обработкой:

function safeRender(selector: string, locale = 'ru'): void {
  const el = document.querySelector(selector);
  if (el === null) {
    console.warn(`Element not found: ${selector}`);
    return;
  }
  render(el, locale);
}

Ошибка: ESM/CJS конфликт типов

При использовании esm-версии библиотеки:

// Ошибка: This expression is not callable
import timeago from 'timeago.js';
timeago.format(date);

Правильный именованный импорт:

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

Ошибка: Overload signature mismatch

// Попытка перегрузки — ошибка
function format(date: Date): string;
function format(date: string): string;

Функции из модуля не перегружаются напрямую. Используется declaration merging:

declare module 'timeago.js' {
  export function format(date: Date): string;
  export function format(date: string): string;
  export function format(date: number): string;
  export function format(date: Date | string | number, locale?: string): string;
}

Проблема: потеря типа после spread

interface Post { id: number; createdAt: string; }

const post: Post = { id: 1, createdAt: '2025-01-01' };
const enriched = { ...post, timeAgo: format(post.createdAt, 'ru') };
// Тип enriched: { id: number; createdAt: string; timeAgo: string; }

Тип не теряется — TypeScript корректно выводит пересечение. Проблема возникает при попытке использовать enriched там, где ожидается Post:

function accept(p: Post) { /* ... */ }
accept(enriched); // OK — лишнее поле timeAgo разрешено при передаче

Проблема: индексный тип в старых версиях

Если объект типизирован через Record:

type PostRecord = Record<string, unknown>;

function formatRecord(record: PostRecord, dateField: string): string {
  const value = record[dateField]; // тип: unknown
  if (typeof value === 'string' || typeof value === 'number' || value instanceof Date) {
    return format(value, 'ru');
  }
  throw new TypeError(`Field "${dateField}" is not a valid date`);
}

Таблица типичных ошибок и решений

Ошибка Причина Решение
null не совместим с DateInput strictNullChecks: true Проверка или ??
unknown не совместим Входные данные без типа Явная проверка типа
Module not found Нет деклараций типов Переустановить или создать .d.ts
LocaleFunc возвращает string Неверный возвращаемый тип Возвращать [string, string]
render принимает null querySelector возвращает null Проверка на null перед вызовом
Default import вместо named Неверный синтаксис импорта Использовать именованные импорты