Расширение базовых типов

TypeScript позволяет расширять существующие типы библиотеки через declaration merging, module augmentation и utility types. Это открывает возможность адаптировать типизацию под конкретные нужды проекта без форка библиотеки.


Declaration Merging для timeago.js

// types/timeago.d.ts
declare module 'timeago.js' {
  // Добавить строгий тип локали
  export type SupportedLocale =
    | 'ru' | 'en_US' | 'de' | 'fr' | 'es'
    | 'zh_CN' | 'zh_TW' | 'ja' | 'ko' | 'ar'
    | 'pt_BR' | 'it' | 'nl' | 'pl' | 'tr';

  // Перегрузка format с ограниченным набором локалей
  export function format(
    date: Date | string | number,
    locale?: SupportedLocale,
    opts?: FormatOptions
  ): string;
}

После добавления этого файла TypeScript будет проверять локаль на этапе компиляции.


Расширение интерфейса FormatOptions

declare module 'timeago.js' {
  interface FormatOptions {
    relativeDate?: Date | number;
    // Добавить собственные поля
    fallback?: string;
    maxAge?: number;
  }
}

Поля fallback и maxAge — кастомные: сама библиотека их не читает, но их можно использовать в обёртке:

import { format } from 'timeago.js';
import type { FormatOptions } from 'timeago.js';

function safeFormat(date: Date | string | number, locale = 'ru', opts?: FormatOptions): string {
  if (opts?.maxAge) {
    const age = Date.now() - new Date(date).getTime();
    if (age > opts.maxAge) {
      return opts.fallback ?? new Date(date).toLocaleDateString('ru');
    }
  }
  return format(date, locale, opts);
}

Utility Types для LocaleFunc

import type { LocaleFunc } from 'timeago.js';

// Получить тип параметров функции локали
type LocaleFuncParams = Parameters<LocaleFunc>;
// [number: number, index: number]

// Получить тип возврата
type LocaleFuncReturn = ReturnType<LocaleFunc>;
// [string, string]

// Частично применённая функция локали (только index)
type IndexedLocaleFunc = (index: number) => [string, string];

Расширение через Intersection Types

import type { LocaleFunc } from 'timeago.js';

// Расширенная функция локали с метаданными
type LocaleFuncWithMeta = LocaleFunc & {
  readonly localeCode: string;
  readonly pluralForms: number;
  validate?: () => boolean;
};

function createLocale(
  localeCode: string,
  fn: LocaleFunc,
  pluralForms = 3
): LocaleFuncWithMeta {
  const extended = fn as LocaleFuncWithMeta;
  Object.defineProperties(extended, {
    localeCode:   { value: localeCode,   writable: false },
    pluralForms:  { value: pluralForms,  writable: false },
    validate:     { value: () => true,   writable: true  },
  });
  return extended;
}

Mapped Types для мультиязычных конфигов

type SupportedLocale = 'ru' | 'en_US' | 'de';

// Конфиг, где каждая локаль обязательна
type LocaleRegistry = {
  [K in SupportedLocale]: import('timeago.js').LocaleFunc;
};

const registry: LocaleRegistry = {
  ru:    (n, i) => ['секунду назад', 'через секунду'],
  en_US: (n, i) => ['just now', 'right now'],
  de:    (n, i) => ['gerade eben', 'gleich'],
};

Conditional Types для проверки локали

type IsRTLLocale<L extends string> =
  L extends 'ar' | 'he' | 'fa' | 'ur' ? true : false;

type LocaleDirection<L extends string> =
  IsRTLLocale<L> extends true ? 'rtl' : 'ltr';

function getDirection<L extends string>(locale: L): LocaleDirection<L> {
  const rtlLocales = new Set(['ar', 'he', 'fa', 'ur']);
  return (rtlLocales.has(locale) ? 'rtl' : 'ltr') as LocaleDirection<L>;
}

// Использование
const dir = getDirection('ar'); // тип: 'rtl'
const ltr = getDirection('ru'); // тип: 'ltr'

Template Literal Types для ключей локалей

type Language  = 'ru' | 'en' | 'de' | 'fr' | 'zh';
type Region    = 'RU' | 'US' | 'DE' | 'FR' | 'CN' | 'TW';

type LocaleKey = `${Language}_${Region}`;
// 'ru_RU' | 'en_US' | 'de_DE' | ...

// Функция с проверкой ключа локали
function registerLocale(key: LocaleKey, fn: import('timeago.js').LocaleFunc): void {
  import('timeago.js').then(({ register }) => register(key, fn));
}

Расширение через Namespace

// Создать собственное пространство имён поверх timeago.js
namespace Timeago {
  export type Locale = 'ru' | 'en_US' | 'de';

  export interface Config {
    locale:      Locale;
    relativeDate?: Date;
    fallback?:   string;
  }

  export interface FormattedDate {
    raw:       Date;
    formatted: string;
    locale:    Locale;
  }
}

import { format } from 'timeago.js';

function formatWithConfig(
  date: Date | string | number,
  config: Timeago.Config
): Timeago.FormattedDate {
  return {
    raw:       new Date(date as any),
    formatted: format(date, config.locale, { relativeDate: config.relativeDate }),
    locale:    config.locale,
  };
}

Extends для сужения типов

import { format } from 'timeago.js';

// Тип, принимающий только будущие даты
type FutureDate = Date & { _future: true };

function toFutureDate(d: Date): FutureDate {
  if (d.getTime() <= Date.now()) {
    throw new TypeError('Date must be in the future');
  }
  return d as FutureDate;
}

function formatFuture(date: FutureDate, locale = 'ru'): string {
  return format(date, locale);
}

Infer для извлечения типов

import { format } from 'timeago.js';

// Извлечь тип первого аргумента format
type FormatInput = Parameters<typeof format>[0];
// Date | string | number

// Извлечь тип возвращаемого значения
type FormatOutput = ReturnType<typeof format>;
// string

// Создать типизированную обёртку с теми же сигнатурами
type FormatWrapper = (...args: Parameters<typeof format>) => FormatOutput;

const wrappedFormat: FormatWrapper = (date, locale, opts) => {
  return format(date, locale, opts);
};

Таблица подходов к расширению типов

Подход Когда использовать
Declaration Merging Изменение публичного API модуля
Intersection Types Добавление свойств к существующему типу
Utility Types Трансформация (Partial, Required, Pick)
Conditional Types Типы, зависящие от значения другого типа
Template Literal Types Строковые ключи с известной структурой
Namespace Группировка связанных типов под одним именем