В 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).