Типизация конфигурации

Явная типизация конфигурационных объектов устраняет магические значения, упрощает рефакторинг и позволяет IDE показывать автодополнение при создании настроек.


Базовый интерфейс конфигурации

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

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

interface TimeagoConfig {
  locale:          SupportedLocale;
  relativeDate?:   Date | number;
  fallback?:       string;
  updateInterval?: number;
}

const defaultConfig: TimeagoConfig = {
  locale:         'ru',
  updateInterval: 60_000,
};

Разделение конфигурации на части

// Настройки форматирования
interface FormatConfig {
  locale:        SupportedLocale;
  relativeDate?: Date | number;
}

// Настройки рендеринга
interface RenderConfig {
  locale:         SupportedLocale;
  updateInterval: number;
  batchSize:      number;
}

// Настройки поведения при ошибках
interface ErrorConfig {
  fallback:    string;
  onError?:    (err: Error) => void;
  throwErrors: boolean;
}

// Объединённая конфигурация
interface TimeagoConfig extends FormatConfig, RenderConfig, ErrorConfig {}

Readonly конфигурация

type ReadonlyConfig = Readonly<{
  locale:         SupportedLocale;
  updateInterval: number;
  batchSize:      number;
}>;

const CONFIG: ReadonlyConfig = {
  locale:         'ru',
  updateInterval: 60_000,
  batchSize:      50,
};

// CONFIG.locale = 'de'; // Ошибка: нельзя изменить readonly

DeepReadonly для вложенных конфигов

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface TimeagoConfig {
  format: {
    locale:   SupportedLocale;
    fallback: string;
  };
  render: {
    interval:  number;
    batchSize: number;
  };
}

const config: DeepReadonly<TimeagoConfig> = {
  format: { locale: 'ru', fallback: 'давно' },
  render: { interval: 60_000, batchSize: 50 },
};

// config.format.locale = 'de'; // Ошибка

Partial конфигурация с merge

import { format } from 'timeago.js';

interface TimeagoConfig {
  locale:         SupportedLocale;
  updateInterval: number;
  batchSize:      number;
  fallback:       string;
}

const DEFAULT_CONFIG: TimeagoConfig = {
  locale:         'ru',
  updateInterval: 60_000,
  batchSize:      50,
  fallback:       'давно',
};

function createConfig(overrides?: Partial<TimeagoConfig>): TimeagoConfig {
  return { ...DEFAULT_CONFIG, ...overrides };
}

// Использование
const config = createConfig({ locale: 'de', batchSize: 100 });

Типизированный Builder

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

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

class TimeagoBuilder {
  private config: {
    locale:         SupportedLocale;
    updateInterval: number;
    batchSize:      number;
  } = {
    locale:         'ru',
    updateInterval: 60_000,
    batchSize:      50,
  };

  setLocale(locale: SupportedLocale): this {
    this.config.locale = locale;
    return this;
  }

  setInterval(ms: number): this {
    if (ms < 1000) throw new RangeError('Interval must be >= 1000ms');
    this.config.updateInterval = ms;
    return this;
  }

  setBatchSize(size: number): this {
    if (size < 1) throw new RangeError('Batch size must be >= 1');
    this.config.batchSize = size;
    return this;
  }

  build(): Readonly<typeof this.config> {
    return Object.freeze({ ...this.config });
  }
}

const config = new TimeagoBuilder()
  .setLocale('de')
  .setInterval(30_000)
  .setBatchSize(100)
  .build();

Конфигурация с валидацией

interface TimeagoConfig {
  locale:         string;
  updateInterval: number;
  maxElements:    number;
}

const VALID_LOCALES = new Set(['ru', 'en_US', 'de', 'fr', 'zh_CN']);

function validateConfig(config: unknown): TimeagoConfig {
  if (typeof config !== 'object' || config === null) {
    throw new TypeError('Config must be an object');
  }

  const c = config as Record<string, unknown>;

  if (typeof c.locale !== 'string' || !VALID_LOCALES.has(c.locale)) {
    throw new TypeError(`Invalid locale: ${c.locale}`);
  }

  if (typeof c.updateInterval !== 'number' || c.updateInterval < 1000) {
    throw new RangeError('updateInterval must be a number >= 1000');
  }

  if (typeof c.maxElements !== 'number' || c.maxElements < 1) {
    throw new RangeError('maxElements must be a number >= 1');
  }

  return c as TimeagoConfig;
}

Конфигурация через environment variables

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

const LOCALE_VALUES: Record<string, SupportedLocale> = {
  ru:    'ru',
  en_US: 'en_US',
  de:    'de',
};

function getLocaleFromEnv(): SupportedLocale {
  const raw = process.env.TIMEAGO_LOCALE ?? '';
  return LOCALE_VALUES[raw] ?? 'ru';
}

interface AppConfig {
  timeago: {
    locale:   SupportedLocale;
    interval: number;
  };
}

const appConfig: AppConfig = {
  timeago: {
    locale:   getLocaleFromEnv(),
    interval: Number(process.env.TIMEAGO_INTERVAL) || 60_000,
  },
};

Конфигурация с зависимыми полями

type TimeagoMode = 'static' | 'live';

type StaticConfig = {
  mode:     'static';
  locale:   SupportedLocale;
};

type LiveConfig = {
  mode:             'live';
  locale:           SupportedLocale;
  updateInterval:   number;
  maxElements?:     number;
};

type TimeagoConfig = StaticConfig | LiveConfig;

function applyConfig(config: TimeagoConfig, el: Element): void {
  if (config.mode === 'static') {
    // Нет таймеров — только format
    el.textContent = format(el.getAttribute('datetime') ?? '', config.locale);
  } else {
    // Живой режим
    render(el as HTMLElement, config.locale);
  }
}

Таблица паттернов типизации конфигурации

Паттерн Применение
Readonly<T> Иммутабельная конфигурация
Partial<T> Переопределение части значений через override
Required<T> Убрать все optional поля — все должны быть заданы
Builder pattern Пошаговое конструирование с валидацией на каждом шаге
Discriminated Union Разные конфиги для разных режимов работы