TGViewer
amorgunov amorgunov @amorgunov · 1.92K subscribers
Post #148 3.11K
Interface merging в TypeScript

В TypeScript есть возможность автоматически мержить интерфейсы с одинаковыми именами в один (в доке это называется более общим понятием «declaration merging»).

Эта возможность, кстати, одна из причин, почему я долгое время в своих проектах предпочитал использование тайп алиасов (type) в качестве основы для описания типов, а не интерфейсов. Чтобы случайно неявно не объединить два интерфейса вместе.

Но есть несколько кейсов, когда расширение интерфейсов является довольно полезной фичей (с типами, объявленными через type это сделать не получится).

Первый кейс – расширение интерфейса внешних библиотек или глобальных переменных без прямого редактирования оригинального кода. Например, подключаем к jest кастомный матчер со своим API (мы уже мигрировали на vitest, но пример с матчерами для jest-тестов первым пришел на ум):

declare global {
namespace jest {
interface Matchers {
toBeCustomTrue(): CustomMatcherResult
}
}
}

// в любом тесте
expect(value).toBeCustomTrue()


Или добавляем какое-то новое поле в объект window (так же расширяем интерфейс Window)

declare global {
interface Window {
chatSdk?: AwesomeChatSdk
}
}


Изначально интерфейсы Window и Matchers объявлены в отдельных пакетах и хранятся в node modules проекта, а данным кодом (declare XXX) мы их расширяем. Причем один интерфейс можно расширять сколько угодно раз, компилятор TypeScript-а все это «проглотит» и сформирует общий интерфейс.

Второе кейс (частный случай первого) – формирование интерфейса lazy-модулей внутри самого приложения. Например, у нас есть DI-контейнер, который изначально пустой и мы инжектим сервисы не глобально в каком-то общем одном файле, а внутри самого сервиса, чтобы сам DI-контейнер не знал о том, какие сервисы внутри мы регистрируем (разделение ответственности и все такое).

Для этого внутри контейнера объявляем пустой интерфейс:

// ~/shared/di/types.ts
export interface DIContainer {}


И расширяем его при инжекте модуля:

// ~/.../themeService.ts
container.register('THEME_SERVICE_TOKEN', themeService)

declare module '~/shared/di/types' {
export interface DIContainer {
THEME_SERVICE_TOKEN?: typeof themeService
}
}


TypeScript это подхватывает и в местах использования DI-контейнера подсказывает зарегистрированные типы. Такой же подход сейчас используется для lazy-слайсов в rtk.

Пример реализации простейшего DI-контейнера можете посмотреть тут (хук useDi + сервисы на preact-signals).
  • 👍 37
  • ❤ 1
More from @amorgunov
  1. Sep 29, 2026Вообще, если разгонять тему собеседований, то текущие форматы на мой взгляд не подходят по…
  2. Sep 24, 2026В июне выступал на CodeFest и рассказывал про то, как готовиться к архитектурной секции (S…
  3. May 25, 2026На прошлой неделе ездил на HolyJS. Про конференции в целом напишу отдельно - есть нескольк…
  4. May 18, 2026Друзья, всем привет! Давно ничего не писал, в очередной раз попробую оживить канал. Обещат…
  5. Jun 12, 2025stringbool в zod Пару дней назад закрыли старый ишьюс от 2022 года в библиотеке zod, в кот…
  6. Jun 6, 2025Мнение – SolidJS За последний год удалось несколько раз пописать код в рамках пет-проектов…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →