Типизация локалей

Строгая типизация локалей позволяет поймать опечатки на этапе компиляции, предоставить автодополнение в IDE и сделать конфигурацию приложения явной. Базовый тип locale?: string слишком широк для production-кода.


Проблема широкого типа string

По умолчанию функция format принимает locale?: string. Это означает, что следующий код скомпилируется без ошибок:

import { format } from 'timeago.js';

format(new Date(), 'ruu');      // опечатка — нет ошибки
format(new Date(), 'english'); // неверное имя — нет ошибки
format(new Date(), '');         // пустая строка — нет ошибки

Базовый union тип для локалей

type Locale =
  | 'af'     // Африкаанс
  | 'ar'     // Арабский
  | 'be'     // Белорусский
  | 'bg'     // Болгарский
  | 'ca'     // Каталанский
  | 'da'     // Датский
  | 'de'     // Немецкий
  | 'el'     // Греческий
  | 'en_US'  // Английский (США)
  | 'es'     // Испанский
  | 'eu'     // Баскский
  | 'fa'     // Персидский
  | 'fi'     // Финский
  | 'fr'     // Французский
  | 'he'     // Иврит
  | 'hr'     // Хорватский
  | 'hu'     // Венгерский
  | 'hy_AM'  // Армянский
  | 'id_ID'  // Индонезийский
  | 'it'     // Итальянский
  | 'ja'     // Японский
  | 'ka'     // Грузинский
  | 'ko'     // Корейский
  | 'ml'     // Малаялам
  | 'my'     // Бирманский
  | 'nb_NO'  // Норвежский
  | 'nl'     // Нидерландский
  | 'nn_NO'  // Нюнорск
  | 'pl'     // Польский
  | 'pt_BR'  // Португальский (Бразилия)
  | 'ro'     // Румынский
  | 'ru'     // Русский
  | 'sk'     // Словацкий
  | 'sl'     // Словенский
  | 'sr'     // Сербский
  | 'sv'     // Шведский
  | 'ta'     // Тамильский
  | 'th'     // Тайский
  | 'tr'     // Турецкий
  | 'uk'     // Украинский
  | 'uz'     // Узбекский
  | 'vi'     // Вьетнамский
  | 'zh_CN'  // Китайский (упрощённый)
  | 'zh_TW'  // Китайский (традиционный)
  | 'zt'     // Канtonsky (кантонский);

Строгая обёртка format

import { format } from 'timeago.js';

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

function typedFormat(
  date: Date | string | number,
  locale: Locale = 'ru'
): string {
  return format(date, locale);
}

// typedFormat(new Date(), 'ruu'); // Ошибка TypeScript
// typedFormat(new Date(), 'ru');  // OK

Тип для подмножества локалей приложения

В реальных приложениях часто поддерживается не весь список, а только несколько языков:

// Полный список из библиотеки
type TimeagoLocale = 'ru' | 'en_US' | 'de' | 'fr' | /* ... */ string;

// Поддерживаемые приложением
type AppLocale = 'ru' | 'en_US' | 'de';

// Проверка совместимости на уровне типов
type ValidAppLocale = AppLocale extends TimeagoLocale ? AppLocale : never;
// Если AppLocale выходит за рамки TimeagoLocale — ошибка типов при использовании

Константа как источник типа

const LOCALES = ['ru', 'en_US', 'de', 'fr', 'es'] as const;

type SupportedLocale = (typeof LOCALES)[number];
// 'ru' | 'en_US' | 'de' | 'fr' | 'es'

function isValidLocale(locale: string): locale is SupportedLocale {
  return (LOCALES as readonly string[]).includes(locale);
}

function safeFormat(date: Date, locale: string): string {
  const resolvedLocale: SupportedLocale = isValidLocale(locale) ? locale : 'ru';
  return format(date, resolvedLocale);
}

Типизация карты языков

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

interface LocaleConfig {
  code:      AppLocale;
  name:      string;
  direction: 'ltr' | 'rtl';
}

const LOCALE_CONFIGS: Record<AppLocale, LocaleConfig> = {
  ru:    { code: 'ru',    name: 'Русский',  direction: 'ltr' },
  en_US: { code: 'en_US', name: 'English',  direction: 'ltr' },
  de:    { code: 'de',    name: 'Deutsch',  direction: 'ltr' },
};

// Функция, принимающая только ключ из конфига
function formatWithConfig(date: Date, localeCode: AppLocale): string {
  const config = LOCALE_CONFIGS[localeCode];
  return format(date, config.code);
}

Discriminated Union для результатов с локалью

import { format } from 'timeago.js';

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

type LocalizedResult =
  | { locale: 'ru';    value: string; direction: 'ltr' }
  | { locale: 'en_US'; value: string; direction: 'ltr' }
  | { locale: 'de';    value: string; direction: 'ltr' };

function formatLocalized(
  date: Date | string | number,
  locale: AppLocale
): LocalizedResult {
  const value = format(date, locale);

  // TypeScript требует exhaustive return
  switch (locale) {
    case 'ru':    return { locale, value, direction: 'ltr' };
    case 'en_US': return { locale, value, direction: 'ltr' };
    case 'de':    return { locale, value, direction: 'ltr' };
  }
}

Функция определения локали из браузера

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

const LOCALE_MAP: Record<string, AppLocale> = {
  'ru':    'ru',
  'ru-RU': 'ru',
  'en':    'en_US',
  'en-US': 'en_US',
  'de':    'de',
  'de-DE': 'de',
};

function detectLocale(fallback: AppLocale = 'ru'): AppLocale {
  const browserLocale = navigator.language;
  return LOCALE_MAP[browserLocale] ?? fallback;
}

// Тип результата — AppLocale, не string
const locale = detectLocale(); // AppLocale

Overloads для разных наборов локалей

import { format } from 'timeago.js';

type RTLLocale = 'ar' | 'he' | 'fa';
type LTRLocale = 'ru' | 'en_US' | 'de';
type AnyLocale = RTLLocale | LTRLocale;

// Перегрузки с разными возвращаемыми типами
function formatWithDirection(date: Date, locale: RTLLocale): { value: string; dir: 'rtl' };
function formatWithDirection(date: Date, locale: LTRLocale): { value: string; dir: 'ltr' };
function formatWithDirection(date: Date, locale: AnyLocale): { value: string; dir: 'rtl' | 'ltr' } {
  const rtlSet = new Set<string>(['ar', 'he', 'fa']);
  return {
    value: format(date, locale),
    dir:   rtlSet.has(locale) ? 'rtl' : 'ltr',
  };
}

const ar = formatWithDirection(new Date(), 'ar');
// ar.dir — тип 'rtl', не 'rtl' | 'ltr'

Declaration Merging для строгой локали

declare module 'timeago.js' {
  type Locale = 'ru' | 'en_US' | 'de' | 'fr' | 'es' | 'zh_CN' | 'ja' | 'ko';

  export function format(
    date: Date | string | number,
    locale?: Locale,
    opts?: FormatOptions
  ): string;

  export function render(
    nodes: Element | NodeList | Element[],
    locale?: Locale
  ): void;
}

Единое место определения — все вызовы format и render во всём проекте начнут проверяться автоматически.